Showing
4 changed files
with
100 additions
and
19 deletions
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | +import 'dart:io'; | ||
| 2 | 3 | ||
| 3 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; | 4 | import 'package:doublefeel_flutter/app/modules/watch_theme/models/watch_theme_models.dart'; |
| 4 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; | 5 | import 'package:doublefeel_flutter/app/routes/app_pages.dart'; |
| @@ -7,15 +8,16 @@ import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | @@ -7,15 +8,16 @@ import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | ||
| 7 | import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; | 8 | import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; |
| 8 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 9 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 9 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; | 10 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; |
| 11 | +import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; | ||
| 10 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 12 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 11 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 13 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 12 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | 14 | import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; |
| 13 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; | 15 | import 'package:doublefeel_flutter/data/models/user/user_models.dart'; |
| 14 | -import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; | 16 | +import 'package:flutter/material.dart'; |
| 17 | +import 'package:flutter/services.dart'; | ||
| 15 | import 'package:get/get.dart'; | 18 | import 'package:get/get.dart'; |
| 16 | import 'package:image_cropper/image_cropper.dart'; | 19 | import 'package:image_cropper/image_cropper.dart'; |
| 17 | import 'package:image_picker/image_picker.dart'; | 20 | import 'package:image_picker/image_picker.dart'; |
| 18 | -import 'package:logger/logger.dart'; | ||
| 19 | 21 | ||
| 20 | class MyController extends GetxController { | 22 | class MyController extends GetxController { |
| 21 | MyController(this._userApi, this._vipApi, this._themeApi); | 23 | MyController(this._userApi, this._vipApi, this._themeApi); |
| @@ -64,22 +66,45 @@ class MyController extends GetxController { | @@ -64,22 +66,45 @@ class MyController extends GetxController { | ||
| 64 | ); | 66 | ); |
| 65 | if (picked == null) return; // 用户取消 | 67 | if (picked == null) return; // 用户取消 |
| 66 | AppLogger.e(picked.path); | 68 | AppLogger.e(picked.path); |
| 67 | - // 2. 用 image_cropper 裁剪为正方形 | ||
| 68 | - final CroppedFile? cropped = await ImageCropper().cropImage( | ||
| 69 | - sourcePath: picked.path, | ||
| 70 | - aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1), | ||
| 71 | - uiSettings: [ | ||
| 72 | - AndroidUiSettings( | ||
| 73 | - toolbarTitle: '裁剪头像', | ||
| 74 | - lockAspectRatio: true, | ||
| 75 | - ), | ||
| 76 | - IOSUiSettings( | ||
| 77 | - title: '裁剪头像', | ||
| 78 | - aspectRatioLockEnabled: true, | ||
| 79 | - resetAspectRatioEnabled: false, | ||
| 80 | - ), | ||
| 81 | - ], | ||
| 82 | - ); | 69 | + // OHOS 版 image_cropper 使用 Flutter 页面承载裁剪 UI,必须显式传入 |
| 70 | + // BuildContext(该插件目前通过 WebUiSettings 读取该参数)。 | ||
| 71 | + final isOhos = Platform.operatingSystem.toLowerCase() == 'ohos'; | ||
| 72 | + final cropperContext = Get.context ?? Get.overlayContext; | ||
| 73 | + CroppedFile? cropped; | ||
| 74 | + try { | ||
| 75 | + cropped = await ImageCropper().cropImage( | ||
| 76 | + sourcePath: picked.path, | ||
| 77 | + aspectRatio: const CropAspectRatio(ratioX: 1, ratioY: 1), | ||
| 78 | + uiSettings: [ | ||
| 79 | + AndroidUiSettings( | ||
| 80 | + toolbarTitle: '裁剪头像', | ||
| 81 | + lockAspectRatio: true, | ||
| 82 | + ), | ||
| 83 | + IOSUiSettings( | ||
| 84 | + title: '裁剪头像', | ||
| 85 | + aspectRatioLockEnabled: true, | ||
| 86 | + resetAspectRatioEnabled: false, | ||
| 87 | + ), | ||
| 88 | + if (isOhos && cropperContext != null) | ||
| 89 | + WebUiSettings(context: cropperContext), | ||
| 90 | + ], | ||
| 91 | + ); | ||
| 92 | + } finally { | ||
| 93 | + if (isOhos) { | ||
| 94 | + // 等待 cropper 路由的退场动画结束,防止其 AppBar 覆盖恢复的样式。 | ||
| 95 | + await Future<void>.delayed(const Duration(milliseconds: 400)); | ||
| 96 | + | ||
| 97 | + SystemChrome.setSystemUIOverlayStyle( | ||
| 98 | + const SystemUiOverlayStyle( | ||
| 99 | + statusBarColor: Colors.transparent, | ||
| 100 | + statusBarIconBrightness: Brightness.dark, | ||
| 101 | + statusBarBrightness: Brightness.light, | ||
| 102 | + systemNavigationBarColor: Colors.transparent, | ||
| 103 | + systemNavigationBarIconBrightness: Brightness.dark, | ||
| 104 | + ), | ||
| 105 | + ); | ||
| 106 | + } | ||
| 107 | + } | ||
| 83 | if (cropped == null) return; // 用户取消裁剪 | 108 | if (cropped == null) return; // 用户取消裁剪 |
| 84 | 109 | ||
| 85 | final ossPath = await _platformHostApi.uploadFile( | 110 | final ossPath = await _platformHostApi.uploadFile( |
| @@ -44,4 +44,12 @@ enum HuaweiHealthDataType { | @@ -44,4 +44,12 @@ enum HuaweiHealthDataType { | ||
| 44 | const HuaweiHealthDataType(this.dataType); | 44 | const HuaweiHealthDataType(this.dataType); |
| 45 | 45 | ||
| 46 | final int dataType; | 46 | final int dataType; |
| 47 | + | ||
| 48 | + /// Resolves the server/native `data_type` value to a supported type. | ||
| 49 | + static HuaweiHealthDataType? fromDataType(int dataType) { | ||
| 50 | + for (final type in values) { | ||
| 51 | + if (type.dataType == dataType) return type; | ||
| 52 | + } | ||
| 53 | + return null; | ||
| 54 | + } | ||
| 47 | } | 55 | } |
| @@ -2,6 +2,7 @@ import 'dart:async'; | @@ -2,6 +2,7 @@ import 'dart:async'; | ||
| 2 | 2 | ||
| 3 | import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; | 3 | import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; |
| 4 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; | 4 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 5 | +import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/huawei_health_data_type.dart'; | ||
| 5 | import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; | 6 | import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart'; |
| 6 | 7 | ||
| 7 | import '../logging/app_logger.dart'; | 8 | import '../logging/app_logger.dart'; |
| @@ -14,6 +15,8 @@ import 'web_socket_service.dart'; | @@ -14,6 +15,8 @@ import 'web_socket_service.dart'; | ||
| 14 | 15 | ||
| 15 | /// Login lifecycle and SDK initialization. | 16 | /// Login lifecycle and SDK initialization. |
| 16 | class UserStateService { | 17 | class UserStateService { |
| 18 | + static const _healthDataUpdateXTag = 'health_data_update'; | ||
| 19 | + | ||
| 17 | UserStateService( | 20 | UserStateService( |
| 18 | this._environmentConfig, | 21 | this._environmentConfig, |
| 19 | this._userPrefs, | 22 | this._userPrefs, |
| @@ -33,6 +36,7 @@ class UserStateService { | @@ -33,6 +36,7 @@ class UserStateService { | ||
| 33 | final ThinkingDataService _thinkingDataService; | 36 | final ThinkingDataService _thinkingDataService; |
| 34 | final HealthRawDataCoreService _healthRawDataCoreService; | 37 | final HealthRawDataCoreService _healthRawDataCoreService; |
| 35 | final WebSocketService _webSocketService; | 38 | final WebSocketService _webSocketService; |
| 39 | + StreamSubscription<WebSocketEvent>? _healthDataUpdateSubscription; | ||
| 36 | 40 | ||
| 37 | bool sdksInitialized = false; | 41 | bool sdksInitialized = false; |
| 38 | 42 | ||
| @@ -53,6 +57,7 @@ class UserStateService { | @@ -53,6 +57,7 @@ class UserStateService { | ||
| 53 | // after login, but must not delay the rest of the authenticated session. | 57 | // after login, but must not delay the rest of the authenticated session. |
| 54 | unawaited(_pushService.registerPushToken()); | 58 | unawaited(_pushService.registerPushToken()); |
| 55 | await _imService.connect(); | 59 | await _imService.connect(); |
| 60 | + _listenForHealthDataUpdates(); | ||
| 56 | _webSocketService.start(); | 61 | _webSocketService.start(); |
| 57 | } | 62 | } |
| 58 | 63 | ||
| @@ -61,6 +66,8 @@ class UserStateService { | @@ -61,6 +66,8 @@ class UserStateService { | ||
| 61 | _pushService.cancelPushTokenRegistration(); | 66 | _pushService.cancelPushTokenRegistration(); |
| 62 | _imService.disconnect(); | 67 | _imService.disconnect(); |
| 63 | _webSocketService.stop(); | 68 | _webSocketService.stop(); |
| 69 | + await _healthDataUpdateSubscription?.cancel(); | ||
| 70 | + _healthDataUpdateSubscription = null; | ||
| 64 | _healthRawDataCoreService.closeDatabase(); | 71 | _healthRawDataCoreService.closeDatabase(); |
| 65 | if (callServerLogout) { | 72 | if (callServerLogout) { |
| 66 | await _userApi.logout(); | 73 | await _userApi.logout(); |
| @@ -74,6 +81,45 @@ class UserStateService { | @@ -74,6 +81,45 @@ class UserStateService { | ||
| 74 | AppLogger.i('UserStateService.onLogout completed'); | 81 | AppLogger.i('UserStateService.onLogout completed'); |
| 75 | } | 82 | } |
| 76 | 83 | ||
| 84 | + void _listenForHealthDataUpdates() { | ||
| 85 | + _healthDataUpdateSubscription ??= _webSocketService | ||
| 86 | + .eventsFor(_healthDataUpdateXTag) | ||
| 87 | + .listen(_onHealthDataUpdate); | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + void _onHealthDataUpdate(WebSocketEvent event) { | ||
| 91 | + final dataType = _healthDataTypeFromBody(event.body); | ||
| 92 | + if (dataType == null) { | ||
| 93 | + AppLogger.w( | ||
| 94 | + 'Ignored health_data_update with an invalid data_type: ${event.body}', | ||
| 95 | + ); | ||
| 96 | + return; | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + final processing = _healthRawDataCoreService.onHealthDataUpdated( | ||
| 100 | + dataTypes: <int>[dataType.dataType], | ||
| 101 | + ); | ||
| 102 | + unawaited(processing.catchError((Object error, StackTrace stackTrace) { | ||
| 103 | + AppLogger.w('Failed to process health_data_update', error, stackTrace); | ||
| 104 | + })); | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + HuaweiHealthDataType? _healthDataTypeFromBody(Object? body) { | ||
| 108 | + if (body is! Map) return null; | ||
| 109 | + final value = body['data_type']; | ||
| 110 | + final dataType = switch (value) { | ||
| 111 | + final int number => number, | ||
| 112 | + final double number | ||
| 113 | + when number.isFinite && number == number.truncateToDouble() => | ||
| 114 | + number.toInt(), | ||
| 115 | + final String text => int.tryParse(text), | ||
| 116 | + _ => null, | ||
| 117 | + }; | ||
| 118 | + return dataType == null | ||
| 119 | + ? null | ||
| 120 | + : HuaweiHealthDataType.fromDataType(dataType); | ||
| 121 | + } | ||
| 122 | + | ||
| 77 | void _trackLogin() { | 123 | void _trackLogin() { |
| 78 | final uid = _userPrefs.preferences.value.meUserInfo?.id ?? 0; | 124 | final uid = _userPrefs.preferences.value.meUserInfo?.id ?? 0; |
| 79 | if (uid > 0) { | 125 | if (uid > 0) { |
| @@ -451,7 +451,9 @@ export class PlatformHostApiImpl extends PlatformHostApi { | @@ -451,7 +451,9 @@ export class PlatformHostApiImpl extends PlatformHostApi { | ||
| 451 | id: notificationId, | 451 | id: notificationId, |
| 452 | content: { | 452 | content: { |
| 453 | notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, | 453 | notificationContentType: notificationManager.ContentType.NOTIFICATION_CONTENT_BASIC_TEXT, |
| 454 | - normal: { title, text: content, additionalText: link }, | 454 | + // The deep link is carried by the WantAgent. Do not render it in the |
| 455 | + // notification body, where query parameters may be exposed to users. | ||
| 456 | + normal: { title, text: content }, | ||
| 455 | }, | 457 | }, |
| 456 | wantAgent: notificationWantAgent, | 458 | wantAgent: notificationWantAgent, |
| 457 | }); | 459 | }); |
-
Please register or login to post a comment