Showing
6 changed files
with
135 additions
and
19 deletions
| @@ -338,6 +338,8 @@ interface PlatformHostApi { | @@ -338,6 +338,8 @@ interface PlatformHostApi { | ||
| 338 | * `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | 338 | * `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` |
| 339 | */ | 339 | */ |
| 340 | fun getFullUserAgent(): String | 340 | fun getFullUserAgent(): String |
| 341 | + /** 申请通知权限 */ | ||
| 342 | + fun requestNotificationAuth(callback: (Result<Boolean>) -> Unit) | ||
| 341 | /** 是否是测试环境 */ | 343 | /** 是否是测试环境 */ |
| 342 | fun isDebugEnvoriment(): Boolean | 344 | fun isDebugEnvoriment(): Boolean |
| 343 | fun shareText(text: String, callback: (Result<Boolean>) -> Unit) | 345 | fun shareText(text: String, callback: (Result<Boolean>) -> Unit) |
| @@ -417,6 +419,24 @@ interface PlatformHostApi { | @@ -417,6 +419,24 @@ interface PlatformHostApi { | ||
| 417 | } | 419 | } |
| 418 | } | 420 | } |
| 419 | run { | 421 | run { |
| 422 | + val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestNotificationAuth$separatedMessageChannelSuffix", codec) | ||
| 423 | + if (api != null) { | ||
| 424 | + channel.setMessageHandler { _, reply -> | ||
| 425 | + api.requestNotificationAuth{ result: Result<Boolean> -> | ||
| 426 | + val error = result.exceptionOrNull() | ||
| 427 | + if (error != null) { | ||
| 428 | + reply.reply(PlatformApiPigeonUtils.wrapError(error)) | ||
| 429 | + } else { | ||
| 430 | + val data = result.getOrNull() | ||
| 431 | + reply.reply(PlatformApiPigeonUtils.wrapResult(data)) | ||
| 432 | + } | ||
| 433 | + } | ||
| 434 | + } | ||
| 435 | + } else { | ||
| 436 | + channel.setMessageHandler(null) | ||
| 437 | + } | ||
| 438 | + } | ||
| 439 | + run { | ||
| 420 | val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment$separatedMessageChannelSuffix", codec) | 440 | val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment$separatedMessageChannelSuffix", codec) |
| 421 | if (api != null) { | 441 | if (api != null) { |
| 422 | channel.setMessageHandler { _, reply -> | 442 | channel.setMessageHandler { _, reply -> |
| @@ -368,6 +368,8 @@ protocol PlatformHostApi { | @@ -368,6 +368,8 @@ protocol PlatformHostApi { | ||
| 368 | /// 返回完整的 User-Agent 字符串,由 native 侧组装: | 368 | /// 返回完整的 User-Agent 字符串,由 native 侧组装: |
| 369 | /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | 369 | /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` |
| 370 | func getFullUserAgent() throws -> String | 370 | func getFullUserAgent() throws -> String |
| 371 | + /// 申请通知权限 | ||
| 372 | + func requestNotificationAuth(completion: @escaping (Result<Bool, Error>) -> Void) | ||
| 371 | /// 是否是测试环境 | 373 | /// 是否是测试环境 |
| 372 | func isDebugEnvoriment() throws -> Bool | 374 | func isDebugEnvoriment() throws -> Bool |
| 373 | func shareText(text: String, completion: @escaping (Result<Bool, Error>) -> Void) | 375 | func shareText(text: String, completion: @escaping (Result<Bool, Error>) -> Void) |
| @@ -434,6 +436,22 @@ class PlatformHostApiSetup { | @@ -434,6 +436,22 @@ class PlatformHostApiSetup { | ||
| 434 | } else { | 436 | } else { |
| 435 | getFullUserAgentChannel.setMessageHandler(nil) | 437 | getFullUserAgentChannel.setMessageHandler(nil) |
| 436 | } | 438 | } |
| 439 | + /// 申请通知权限 | ||
| 440 | + let requestNotificationAuthChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestNotificationAuth\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||
| 441 | + if let api = api { | ||
| 442 | + requestNotificationAuthChannel.setMessageHandler { _, reply in | ||
| 443 | + api.requestNotificationAuth { result in | ||
| 444 | + switch result { | ||
| 445 | + case .success(let res): | ||
| 446 | + reply(wrapResult(res)) | ||
| 447 | + case .failure(let error): | ||
| 448 | + reply(wrapError(error)) | ||
| 449 | + } | ||
| 450 | + } | ||
| 451 | + } | ||
| 452 | + } else { | ||
| 453 | + requestNotificationAuthChannel.setMessageHandler(nil) | ||
| 454 | + } | ||
| 437 | /// 是否是测试环境 | 455 | /// 是否是测试环境 |
| 438 | let isDebugEnvorimentChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | 456 | let isDebugEnvorimentChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) |
| 439 | if let api = api { | 457 | if let api = api { |
| @@ -903,11 +903,9 @@ class LocalHealthDataConvert { | @@ -903,11 +903,9 @@ class LocalHealthDataConvert { | ||
| 903 | List<HealthRawSleepResult> sleepResults, { | 903 | List<HealthRawSleepResult> sleepResults, { |
| 904 | required Iterable<DateTime> allowedSleepDays, | 904 | required Iterable<DateTime> allowedSleepDays, |
| 905 | }) { | 905 | }) { |
| 906 | - final bedtime = _extremeBedtime( | ||
| 907 | - _bedtimesBySleepDay( | 906 | + final bedtime = _extremeSleepResultBedtime( |
| 907 | + _sleepResultBedtimes( | ||
| 908 | sleepResults, | 908 | sleepResults, |
| 909 | - (item) => item.startDate, | ||
| 910 | - earliest: true, | ||
| 911 | allowedSleepDays: allowedSleepDays, | 909 | allowedSleepDays: allowedSleepDays, |
| 912 | ), | 910 | ), |
| 913 | earliest: true, | 911 | earliest: true, |
| @@ -923,11 +921,9 @@ class LocalHealthDataConvert { | @@ -923,11 +921,9 @@ class LocalHealthDataConvert { | ||
| 923 | List<HealthRawSleepResult> sleepResults, { | 921 | List<HealthRawSleepResult> sleepResults, { |
| 924 | required Iterable<DateTime> allowedSleepDays, | 922 | required Iterable<DateTime> allowedSleepDays, |
| 925 | }) { | 923 | }) { |
| 926 | - final bedtime = _extremeBedtime( | ||
| 927 | - _bedtimesBySleepDay( | 924 | + final bedtime = _extremeSleepResultBedtime( |
| 925 | + _sleepResultBedtimes( | ||
| 928 | sleepResults, | 926 | sleepResults, |
| 929 | - (item) => item.startDate, | ||
| 930 | - earliest: false, | ||
| 931 | allowedSleepDays: allowedSleepDays, | 927 | allowedSleepDays: allowedSleepDays, |
| 932 | ), | 928 | ), |
| 933 | earliest: false, | 929 | earliest: false, |
| @@ -939,6 +935,41 @@ class LocalHealthDataConvert { | @@ -939,6 +935,41 @@ class LocalHealthDataConvert { | ||
| 939 | ); | 935 | ); |
| 940 | } | 936 | } |
| 941 | 937 | ||
| 938 | + static List<_SleepResultBedtime> _sleepResultBedtimes( | ||
| 939 | + Iterable<HealthRawSleepResult> sleepResults, { | ||
| 940 | + required Iterable<DateTime> allowedSleepDays, | ||
| 941 | + }) { | ||
| 942 | + final allowedSleepDayKeys = { | ||
| 943 | + for (final day in allowedSleepDays) dateKey(day), | ||
| 944 | + }; | ||
| 945 | + return [ | ||
| 946 | + for (final result in sleepResults) | ||
| 947 | + if (allowedSleepDayKeys.contains(dateKey(dayOf(result.date)))) | ||
| 948 | + _SleepResultBedtime( | ||
| 949 | + timestamp: result.startDate, | ||
| 950 | + sleepDay: dayOf(result.date), | ||
| 951 | + sleepStartOffset: | ||
| 952 | + result.startDate - unixSeconds(dayOf(result.date)), | ||
| 953 | + ), | ||
| 954 | + ]; | ||
| 955 | + } | ||
| 956 | + | ||
| 957 | + static _SleepResultBedtime? _extremeSleepResultBedtime( | ||
| 958 | + Iterable<_SleepResultBedtime> bedtimes, { | ||
| 959 | + required bool earliest, | ||
| 960 | + }) { | ||
| 961 | + _SleepResultBedtime? selected; | ||
| 962 | + for (final bedtime in bedtimes) { | ||
| 963 | + if (selected == null || | ||
| 964 | + (earliest | ||
| 965 | + ? bedtime.sleepStartOffset < selected.sleepStartOffset | ||
| 966 | + : bedtime.sleepStartOffset > selected.sleepStartOffset)) { | ||
| 967 | + selected = bedtime; | ||
| 968 | + } | ||
| 969 | + } | ||
| 970 | + return selected; | ||
| 971 | + } | ||
| 972 | + | ||
| 942 | static LatestSleepInfo? _latestSleepInfo( | 973 | static LatestSleepInfo? _latestSleepInfo( |
| 943 | List<HealthSleepDayCalculation> daily, { | 974 | List<HealthSleepDayCalculation> daily, { |
| 944 | required Iterable<DateTime> allowedSleepDays, | 975 | required Iterable<DateTime> allowedSleepDays, |
| @@ -1176,3 +1207,15 @@ class _SleepBedtime { | @@ -1176,3 +1207,15 @@ class _SleepBedtime { | ||
| 1176 | final DateTime sleepDay; | 1207 | final DateTime sleepDay; |
| 1177 | final int normalizedMinutes; | 1208 | final int normalizedMinutes; |
| 1178 | } | 1209 | } |
| 1210 | + | ||
| 1211 | +class _SleepResultBedtime { | ||
| 1212 | + const _SleepResultBedtime({ | ||
| 1213 | + required this.timestamp, | ||
| 1214 | + required this.sleepDay, | ||
| 1215 | + required this.sleepStartOffset, | ||
| 1216 | + }); | ||
| 1217 | + | ||
| 1218 | + final int timestamp; | ||
| 1219 | + final DateTime sleepDay; | ||
| 1220 | + final int sleepStartOffset; | ||
| 1221 | +} |
| @@ -393,6 +393,35 @@ class PlatformHostApi { | @@ -393,6 +393,35 @@ class PlatformHostApi { | ||
| 393 | } | 393 | } |
| 394 | } | 394 | } |
| 395 | 395 | ||
| 396 | + /// 申请通知权限 | ||
| 397 | + Future<bool> requestNotificationAuth() async { | ||
| 398 | + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.requestNotificationAuth$pigeonVar_messageChannelSuffix'; | ||
| 399 | + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( | ||
| 400 | + pigeonVar_channelName, | ||
| 401 | + pigeonChannelCodec, | ||
| 402 | + binaryMessenger: pigeonVar_binaryMessenger, | ||
| 403 | + ); | ||
| 404 | + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||
| 405 | + final List<Object?>? pigeonVar_replyList = | ||
| 406 | + await pigeonVar_sendFuture as List<Object?>?; | ||
| 407 | + if (pigeonVar_replyList == null) { | ||
| 408 | + throw _createConnectionError(pigeonVar_channelName); | ||
| 409 | + } else if (pigeonVar_replyList.length > 1) { | ||
| 410 | + throw PlatformException( | ||
| 411 | + code: pigeonVar_replyList[0]! as String, | ||
| 412 | + message: pigeonVar_replyList[1] as String?, | ||
| 413 | + details: pigeonVar_replyList[2], | ||
| 414 | + ); | ||
| 415 | + } else if (pigeonVar_replyList[0] == null) { | ||
| 416 | + throw PlatformException( | ||
| 417 | + code: 'null-error', | ||
| 418 | + message: 'Host platform returned null value for non-null return value.', | ||
| 419 | + ); | ||
| 420 | + } else { | ||
| 421 | + return (pigeonVar_replyList[0] as bool?)!; | ||
| 422 | + } | ||
| 423 | + } | ||
| 424 | + | ||
| 396 | /// 是否是测试环境 | 425 | /// 是否是测试环境 |
| 397 | Future<bool> isDebugEnvoriment() async { | 426 | Future<bool> isDebugEnvoriment() async { |
| 398 | final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment$pigeonVar_messageChannelSuffix'; | 427 | final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isDebugEnvoriment$pigeonVar_messageChannelSuffix'; |
| @@ -116,6 +116,10 @@ abstract class PlatformHostApi { | @@ -116,6 +116,10 @@ abstract class PlatformHostApi { | ||
| 116 | /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | 116 | /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` |
| 117 | String getFullUserAgent(); | 117 | String getFullUserAgent(); |
| 118 | 118 | ||
| 119 | + /// 申请通知权限 | ||
| 120 | + @async | ||
| 121 | + bool requestNotificationAuth(); | ||
| 122 | + | ||
| 119 | /// 是否是测试环境 | 123 | /// 是否是测试环境 |
| 120 | bool isDebugEnvoriment(); | 124 | bool isDebugEnvoriment(); |
| 121 | 125 |
| @@ -119,7 +119,7 @@ void main() { | @@ -119,7 +119,7 @@ void main() { | ||
| 119 | expect(statistics.avgSleepEvaluate, 2); | 119 | expect(statistics.avgSleepEvaluate, 2); |
| 120 | }); | 120 | }); |
| 121 | 121 | ||
| 122 | - test('compares weekly bedtime extremes by sleep-day clock time', () { | 122 | + test('compares result bedtime extremes by sleep-end-day start offset', () { |
| 123 | final july27 = DateTime(2026, 7, 27); | 123 | final july27 = DateTime(2026, 7, 27); |
| 124 | final july28 = DateTime(2026, 7, 28); | 124 | final july28 = DateTime(2026, 7, 28); |
| 125 | final july29 = DateTime(2026, 7, 29); | 125 | final july29 = DateTime(2026, 7, 29); |
| @@ -155,12 +155,12 @@ void main() { | @@ -155,12 +155,12 @@ void main() { | ||
| 155 | statistics.earliestSleepInfo?.asleepTime, | 155 | statistics.earliestSleepInfo?.asleepTime, |
| 156 | LocalHealthDataConvert.unixSeconds(earliestBedtime), | 156 | LocalHealthDataConvert.unixSeconds(earliestBedtime), |
| 157 | ); | 157 | ); |
| 158 | - expect(statistics.earliestSleepInfo?.timeKey, 20260728); | 158 | + expect(statistics.earliestSleepInfo?.timeKey, 20260729); |
| 159 | expect( | 159 | expect( |
| 160 | statistics.latestSleepInfo?.asleepTime, | 160 | statistics.latestSleepInfo?.asleepTime, |
| 161 | LocalHealthDataConvert.unixSeconds(latestBedtime), | 161 | LocalHealthDataConvert.unixSeconds(latestBedtime), |
| 162 | ); | 162 | ); |
| 163 | - expect(statistics.latestSleepInfo?.timeKey, 20260727); | 163 | + expect(statistics.latestSleepInfo?.timeKey, 20260728); |
| 164 | }); | 164 | }); |
| 165 | 165 | ||
| 166 | test('uses the first and last records within a sleep day', () { | 166 | test('uses the first and last records within a sleep day', () { |
| @@ -201,11 +201,11 @@ void main() { | @@ -201,11 +201,11 @@ void main() { | ||
| 201 | statistics.latestSleepInfo?.asleepTime, | 201 | statistics.latestSleepInfo?.asleepTime, |
| 202 | LocalHealthDataConvert.unixSeconds(continuedSleep), | 202 | LocalHealthDataConvert.unixSeconds(continuedSleep), |
| 203 | ); | 203 | ); |
| 204 | - expect(statistics.earliestSleepInfo?.timeKey, 20260728); | ||
| 205 | - expect(statistics.latestSleepInfo?.timeKey, 20260728); | 204 | + expect(statistics.earliestSleepInfo?.timeKey, 20260729); |
| 205 | + expect(statistics.latestSleepInfo?.timeKey, 20260729); | ||
| 206 | }); | 206 | }); |
| 207 | 207 | ||
| 208 | - test('uses noon as the sleep day cutoff', () { | 208 | + test('keeps negative sleep start offset from sleep end day', () { |
| 209 | final july28 = DateTime(2026, 7, 28); | 209 | final july28 = DateTime(2026, 7, 28); |
| 210 | final july29 = DateTime(2026, 7, 29); | 210 | final july29 = DateTime(2026, 7, 29); |
| 211 | final july30 = DateTime(2026, 7, 30); | 211 | final july30 = DateTime(2026, 7, 30); |
| @@ -242,15 +242,15 @@ void main() { | @@ -242,15 +242,15 @@ void main() { | ||
| 242 | statistics.earliestSleepInfo?.asleepTime, | 242 | statistics.earliestSleepInfo?.asleepTime, |
| 243 | LocalHealthDataConvert.unixSeconds(atNoon), | 243 | LocalHealthDataConvert.unixSeconds(atNoon), |
| 244 | ); | 244 | ); |
| 245 | - expect(statistics.earliestSleepInfo?.timeKey, 20260729); | 245 | + expect(statistics.earliestSleepInfo?.timeKey, 20260730); |
| 246 | expect( | 246 | expect( |
| 247 | statistics.latestSleepInfo?.asleepTime, | 247 | statistics.latestSleepInfo?.asleepTime, |
| 248 | LocalHealthDataConvert.unixSeconds(beforeNoon), | 248 | LocalHealthDataConvert.unixSeconds(beforeNoon), |
| 249 | ); | 249 | ); |
| 250 | - expect(statistics.latestSleepInfo?.timeKey, 20260728); | 250 | + expect(statistics.latestSleepInfo?.timeKey, 20260729); |
| 251 | }); | 251 | }); |
| 252 | 252 | ||
| 253 | - test('excludes records outside the report sleep-day range', () { | 253 | + test('keeps records when sleep end day is inside the report range', () { |
| 254 | final july29 = DateTime(2026, 7, 29); | 254 | final july29 = DateTime(2026, 7, 29); |
| 255 | final overnightStart = DateTime(2026, 7, 29, 3); | 255 | final overnightStart = DateTime(2026, 7, 29, 3); |
| 256 | final result = HealthRawSleepResult( | 256 | final result = HealthRawSleepResult( |
| @@ -274,8 +274,10 @@ void main() { | @@ -274,8 +274,10 @@ void main() { | ||
| 274 | heartRate: const [], | 274 | heartRate: const [], |
| 275 | ); | 275 | ); |
| 276 | 276 | ||
| 277 | - expect(statistics.earliestSleepInfo, isNull); | ||
| 278 | - expect(statistics.latestSleepInfo, isNull); | 277 | + expect(statistics.earliestSleepInfo?.asleepTime, result.startDate); |
| 278 | + expect(statistics.latestSleepInfo?.asleepTime, result.startDate); | ||
| 279 | + expect(statistics.earliestSleepInfo?.timeKey, 20260729); | ||
| 280 | + expect(statistics.latestSleepInfo?.timeKey, 20260729); | ||
| 279 | }); | 281 | }); |
| 280 | 282 | ||
| 281 | test('sleep duration excludes awake intervals inside sleep range', () { | 283 | test('sleep duration excludes awake intervals inside sleep range', () { |
-
Please register or login to post a comment