Commit f0bf6ad87202da7ddf4bcf43414ea0bfbf048b26

Authored by 权海
1 parent 21c67bdd

feat(ui):添加apns权限请求,修正睡眠最早最晚算法

@@ -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', () {