Commit 60a95593cb9bf728df8ff64dc797a7264445fb76

Authored by 刘宏哲
1 parent d13cd4c8

feat(app): bug fixed

... ... @@ -36,6 +36,10 @@ class HealthKitHostApiImpl(
}
override fun cancelAuthorizations(callback: (Result<Boolean>) -> Unit) {
callback(Result.success(false))
}
override fun syncHealthDataToCloud(callback: (Result<Boolean>) -> Unit) {
callback(Result.success(false))
}
... ...
... ... @@ -240,6 +240,8 @@ interface HealthKitHostApi {
/** Opens Huawei Health client authorization UI. Returns whether user granted. */
fun checkHealthAppAuthorization(callback: (Result<HealthAuthorization>) -> Unit)
fun requestHealthClientAuthorization(callback: (Result<Boolean>) -> Unit)
/** Cancels all Huawei Health authorizations on OHOS. */
fun cancelAuthorizations(callback: (Result<Boolean>) -> Unit)
/**
* Requests Huawei Health to upload the user's latest data to its cloud.
*
... ... @@ -298,6 +300,24 @@ interface HealthKitHostApi {
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelAuthorizations$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
api.cancelAuthorizations{ result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(HealthKitApiPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(HealthKitApiPigeonUtils.wrapResult(data))
}
}
}
} else {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.syncHealthDataToCloud$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
... ...
... ... @@ -290,6 +290,8 @@ protocol HealthKitHostApi {
/// Opens Huawei Health client authorization UI. Returns whether user granted.
func checkHealthAppAuthorization(completion: @escaping (Result<HealthAuthorization, Error>) -> Void)
func requestHealthClientAuthorization(completion: @escaping (Result<Bool, Error>) -> Void)
/// Cancels all Huawei Health authorizations on OHOS.
func cancelAuthorizations(completion: @escaping (Result<Bool, Error>) -> Void)
/// Requests Huawei Health to upload the user's latest data to its cloud.
///
/// This only triggers the Health app's configured device-to-cloud sync; it
... ... @@ -338,6 +340,22 @@ class HealthKitHostApiSetup {
} else {
requestHealthClientAuthorizationChannel.setMessageHandler(nil)
}
/// Cancels all Huawei Health authorizations on OHOS.
let cancelAuthorizationsChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelAuthorizations\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
cancelAuthorizationsChannel.setMessageHandler { _, reply in
api.cancelAuthorizations { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
cancelAuthorizationsChannel.setMessageHandler(nil)
}
/// Requests Huawei Health to upload the user's latest data to its cloud.
///
/// This only triggers the Health app's configured device-to-cloud sync; it
... ...
... ... @@ -52,6 +52,10 @@ final class HealthKitHostApiImpl: HealthKitHostApi {
}
}
func cancelAuthorizations(completion: @escaping (Result<Bool, Error>) -> Void) {
completion(.success(false))
}
func syncHealthDataToCloud(completion: @escaping (Result<Bool, Error>) -> Void) {
// Huawei Health manual cloud synchronization is HarmonyOS-specific.
completion(.success(false))
... ...
import 'dart:async';
import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
import 'package:doublefeel_flutter/app/modules/friends/controllers/friend_trend_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/home_controller.dart';
import 'package:doublefeel_flutter/app/modules/home/controllers/trend/trend_controller.dart';
... ... @@ -12,8 +13,10 @@ import 'package:doublefeel_flutter/app/routes/app_pages.dart';
import 'package:doublefeel_flutter/app/utils/platform_compact.dart';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
import 'package:doublefeel_flutter/core/error/app_error.dart';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/harmony_api.dart';
import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
... ... @@ -101,6 +104,7 @@ class TodayController extends GetxController with WidgetsBindingObserver {
final stressSubtitle = ''.obs;
bool _isFirstLoad = true;
bool _isHandlingHealthAuthorizationTap = false;
bool _isUnbindingHuaweiHealth = false;
// ── HRV 表盘引导 Banner ───────────────────
final showHrvAdBanner = false.obs;
... ... @@ -336,11 +340,39 @@ class TodayController extends GetxController with WidgetsBindingObserver {
}
Future<void> _showHuaweiHealthRegistrationDialog() async {
final confirmed = await HuaweiHealthRegistrationDialog.show();
if (confirmed) {
final action = await HuaweiHealthRegistrationDialog.show();
if (action == DialogAction.confirm) {
await _platformHostApi.nativeHandleUrl(
'huaweischeme://healthapp/home/main',
);
} else if (action == DialogAction.cancel) {
await _unbindHuaweiHealth();
}
}
Future<void> _unbindHuaweiHealth() async {
if (_isUnbindingHuaweiHealth) {
return;
}
_isUnbindingHuaweiHealth = true;
try {
final result = await Get.find<HarmonyApi>().deletePrivacyRecords();
switch (result) {
case AppSuccess():
AppToast.show('已解除华为健康绑定');
await _checkHealthAuthStatus();
case AppFailure(:final error):
// 登录失效及服务端错误文案已由 safeCall 的全局处理器处理。
if (error is AppCancelledError ||
error.isAccessTokenInvalid ||
(error is AppHttpError &&
(error.businessMessage?.isNotEmpty ?? false))) {
return;
}
AppToast.show('解除绑定失败,请稍后重试');
}
} finally {
_isUnbindingHuaweiHealth = false;
}
}
... ... @@ -456,6 +488,7 @@ class TodayController extends GetxController with WidgetsBindingObserver {
return;
}
await Get.to(() => NoHealthDataPage(
onUnbind: _unbindHuaweiHealth,
onRefresh: () {
refreshTab();
Future.delayed(const Duration(milliseconds: 800), () {
... ...
... ... @@ -8,49 +8,47 @@ import 'package:flutter/material.dart';
/// Registration guidance shown before leaving the app for Huawei Health.
class HuaweiHealthRegistrationDialog extends StatelessWidget {
const HuaweiHealthRegistrationDialog({super.key, required this.onConfirm});
const HuaweiHealthRegistrationDialog({super.key, required this.onAction});
final VoidCallback onConfirm;
final ValueChanged<ActionResult<DialogAction, DialogMetaData>> onAction;
static const _data = DialogMetaData(
iconAsset: null,
title: '该账号暂未注册华为运动健康',
message: '请注册后再进行授权',
confirmText: '好的',
confirmText: '去注册',
cancelText: '解除绑定该账号',
isCancelable: false,
);
static Future<bool> show() async {
static Future<DialogAction> show() async {
final result = await DialogUtils.showDialog<DialogAction, DialogMetaData>(
(data, callback) => HuaweiHealthRegistrationDialog(
onConfirm: () => callback(const ActionResult(
action: DialogAction.confirm,
data: _data,
)),
onAction: callback,
),
_data,
barrierDismissible: false,
dismissAction: DialogAction.dismiss,
);
return result.action == DialogAction.confirm;
return result.action ?? DialogAction.dismiss;
}
static const _titleStyle = TextStyle(
color: Color(0xFF141414),
fontSize: 18,
fontWeight: FontWeight.w600,
height: 1.2,
height: 1.4,
);
static const _messageStyle = TextStyle(
color: Color(0xFF6E6D80),
fontSize: 15,
height: 1.2,
height: 1.4,
);
@override
Widget build(BuildContext context) => CommonDialogView(
dialogMetaData: _data,
onAction: (_) => onConfirm(),
onAction: onAction,
contentPadding: EdgeInsets.fromLTRB(24.dp, 24.dp, 24.dp, 20.dp),
titleWidth: 210.dp,
titleSpacing: 16.dp,
... ... @@ -58,5 +56,7 @@ class HuaweiHealthRegistrationDialog extends StatelessWidget {
titleStyle: _titleStyle,
messageStyle: _messageStyle,
confirmFontWeight: FontWeight.w500,
cancelFontWeight: FontWeight.w500,
cancelTextColor: const Color(0xFF6E6D80),
);
}
... ...
... ... @@ -14,10 +14,12 @@ class NoHealthDataPage extends StatelessWidget {
super.key,
this.onRefresh,
this.onHelp,
this.onUnbind,
});
final VoidCallback? onRefresh;
final VoidCallback? onHelp;
final VoidCallback? onUnbind;
@override
Widget build(BuildContext context) {
... ... @@ -43,7 +45,7 @@ class NoHealthDataPage extends StatelessWidget {
20 + MediaQuery.paddingOf(context).bottom,
),
children: [
const _PageHeading(),
_PageHeading(onUnbind: onUnbind),
const SizedBox(height: 24),
_InfoCard(
path: isOhos()
... ... @@ -150,7 +152,9 @@ class _TopBar extends StatelessWidget {
}
class _PageHeading extends StatelessWidget {
const _PageHeading();
const _PageHeading({this.onUnbind});
final VoidCallback? onUnbind;
@override
Widget build(BuildContext context) {
... ... @@ -185,6 +189,41 @@ class _PageHeading extends StatelessWidget {
),
),
),
if (isOhos()) ...[
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Wrap(
alignment: WrapAlignment.center,
children: [
Text(
'如需解除绑定,点此',
style: TextStyle(
color: context.colors.textSecondary,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 17 / 12,
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onUnbind,
child: Text(
'解除华为健康绑定',
style: TextStyle(
color: context.colors.primary,
fontSize: 12,
fontWeight: FontWeight.w400,
height: 17 / 12,
decoration: TextDecoration.underline,
decorationColor: context.colors.primary,
),
),
),
],
),
),
],
],
);
}
... ...
... ... @@ -20,6 +20,8 @@ class CommonDialogView extends StatelessWidget {
this.titleStyle,
this.messageStyle,
this.confirmFontWeight = FontWeight.w600,
this.cancelFontWeight = FontWeight.w400,
this.cancelTextColor = AppColors.textSecondary,
});
final DialogMetaData dialogMetaData;
... ... @@ -32,6 +34,8 @@ class CommonDialogView extends StatelessWidget {
final TextStyle? titleStyle;
final TextStyle? messageStyle;
final FontWeight confirmFontWeight;
final FontWeight cancelFontWeight;
final Color cancelTextColor;
@override
Widget build(BuildContext context) {
... ... @@ -119,8 +123,8 @@ class CommonDialogView extends StatelessWidget {
SizedBox(height: 4.h),
_DialogButton(
text: dialogMetaData.cancelText,
textColor: AppColors.textSecondary,
fontWeight: FontWeight.w400,
textColor: cancelTextColor,
fontWeight: cancelFontWeight,
onTap: () {
onAction?.call(
ActionResult(
... ...
... ... @@ -42,6 +42,14 @@ class HarmonyApi {
);
}
Future<AppResult<void>> deletePrivacyRecords() {
return safeCall(
call: () async {
await _dioClient.dio.delete(ApiPaths.hmPrivacyRecords);
},
);
}
/// startDate: 开始日期20260801
/// endDate: 截止日期20260801,和开始日期最大相隔31天
Future<AppResult<HmHealthData>> getHealthData(
... ...
... ... @@ -295,6 +295,13 @@ class AppHealthKitHostApi extends _AppPigeonApiFacade {
};
});
/// Cancels all Huawei Health authorizations on HarmonyOS.
Future<bool> cancelAuthorizations() => dispatch(
ios: () async => false,
android: () async => false,
ohos: () => _api.cancelAuthorizations(),
);
/// Requests Huawei Health to synchronize its local data to the cloud.
///
/// Available on HarmonyOS only. A `true` result means the request was
... ...
... ... @@ -298,6 +298,35 @@ class HealthKitHostApi {
}
}
/// Cancels all Huawei Health authorizations on OHOS.
Future<bool> cancelAuthorizations() async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelAuthorizations$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as bool?)!;
}
}
/// Requests Huawei Health to upload the user's latest data to its cloud.
///
/// This only triggers the Health app's configured device-to-cloud sync; it
... ...
... ... @@ -92,6 +92,16 @@ export class HealthKitHostApiImpl extends HealthKitHostApi {
});
}
cancelAuthorizations(result: Result<boolean>): void {
this.ensureInitialized()
.then(() => healthStore.cancelAuthorizations())
.then(() => result.success(true))
.catch((error: BusinessError) => {
console.error(`cancelAuthorizations failed: ${error.code}, ${error.message}`);
result.success(false);
});
}
readActivityGoal(result: Result<HealthActivityGoal | undefined>): void {
this.ensureInitialized()
.then(() => healthService.workout.readActivityReport())
... ...
... ... @@ -327,6 +327,8 @@ export abstract class HealthKitHostApi {
abstract checkHealthAppAuthorization(result: Result<HealthAuthorization>): void;
abstract requestHealthClientAuthorization(result: Result<boolean>): void;
/* Cancels all Huawei Health authorizations on OHOS.*/
abstract cancelAuthorizations(result: Result<boolean>): void;
/*
* Requests Huawei Health to upload the user's latest data to its cloud.
*
... ... @@ -402,6 +404,33 @@ export abstract class HealthKitHostApi {
{
let channel: BasicMessageChannel<Object> =
new BasicMessageChannel(
binaryMessenger, 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelAuthorizations' + separatedMessageChannelSuffix, HealthKitHostApi.getCodec());
if (api != null) {
channel.setMessageHandler({
onMessage(message: Object, reply: Reply<Object>) {
class ResultImp implements Result<boolean>{
success(result: boolean): void {
let res: Array<Object | null> = [];
res.push(result);
reply.reply(res);
}
error(error: Error): void {
let wrappedError: Array<Object | null> = wrapError(error);
reply.reply(wrappedError);
}
}
let resultCallback: Result<boolean> = new ResultImp();
api!.cancelAuthorizations(resultCallback);
} });
} else {
channel.setMessageHandler(null);
}
}
{
let channel: BasicMessageChannel<Object> =
new BasicMessageChannel(
binaryMessenger, 'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.syncHealthDataToCloud' + separatedMessageChannelSuffix, HealthKitHostApi.getCodec());
if (api != null) {
channel.setMessageHandler({
... ...
... ... @@ -68,6 +68,10 @@ abstract class HealthKitHostApi {
@async
bool requestHealthClientAuthorization();
/// Cancels all Huawei Health authorizations on OHOS.
@async
bool cancelAuthorizations();
/// Requests Huawei Health to upload the user's latest data to its cloud.
///
/// This only triggers the Health app's configured device-to-cloud sync; it
... ...
import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
import 'dart:async';
import 'package:doublefeel_flutter/core/error/app_error.dart';
... ... @@ -31,7 +32,7 @@ void main() {
testWidgets('registration dialog waits for confirmation', (tester) async {
addTearDown(Get.reset);
bool? confirmed;
DialogAction? confirmed;
await tester.pumpWidget(GetMaterialApp(
home: Scaffold(
body: TextButton(
... ... @@ -49,11 +50,33 @@ void main() {
await tester.tapAt(const Offset(10, 10));
await tester.pumpAndSettle();
expect(confirmed, isNull);
expect(find.text('好的'), findsOneWidget);
await tester.tap(find.text('好的'));
expect(find.text('去注册'), findsOneWidget);
await tester.tap(find.text('去注册'));
await tester.pumpAndSettle();
expect(confirmed, isTrue);
expect(find.text('好的'), findsNothing);
expect(confirmed, DialogAction.confirm);
expect(find.text('去注册'), findsNothing);
expect(tester.takeException(), isNull);
});
testWidgets('unbind returns a distinct action without registering',
(tester) async {
addTearDown(Get.reset);
DialogAction? action;
await tester.pumpWidget(GetMaterialApp(
home: Scaffold(
body: TextButton(
onPressed: () async {
action = await HuaweiHealthRegistrationDialog.show();
},
child: const Text('show'),
)),
));
await tester.tap(find.text('show'));
await tester.pumpAndSettle();
await tester.tap(find.text('解除绑定该账号'));
await tester.pumpAndSettle();
expect(action, DialogAction.cancel);
expect(find.text('去注册'), findsNothing);
expect(tester.takeException(), isNull);
});
... ... @@ -145,7 +168,7 @@ void main() {
}
expect(openedUrls, isEmpty);
// The facade reports the result without displaying UI or opening apps.
expect(find.text('好的'), findsNothing);
expect(find.text('去注册'), findsNothing);
expect(
authorization,
switch (error) {
... ...
import 'package:doublefeel_flutter/core/platform/pigeon_api_facade.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart' as native;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/services.dart';
class _HealthApi extends native.HealthKitHostApi {
_HealthApi(this.status, {this.granted = false});
... ... @@ -17,6 +18,36 @@ class _HealthApi extends native.HealthKitHostApi {
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('cancel authorization dispatches to OHOS and preserves failures',
() async {
const channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.doublefeel_flutter.HealthKitHostApi.cancelAuthorizations',
StandardMessageCodec(),
);
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
var calls = 0;
var succeeds = true;
messenger.setMockDecodedMessageHandler<Object?>(channel, (_) async {
calls++;
return [succeeds];
});
addTearDown(
() => messenger.setMockDecodedMessageHandler<Object?>(channel, null));
final api = AppHealthKitHostApi(appPlatform: AppPigeonPlatform.ohos);
expect(await api.cancelAuthorizations(), isTrue);
succeeds = false;
expect(await api.cancelAuthorizations(), isFalse);
for (final platform in [AppPigeonPlatform.ios, AppPigeonPlatform.android]) {
expect(
await AppHealthKitHostApi(appPlatform: platform)
.cancelAuthorizations(),
isFalse);
}
expect(calls, 2);
});
test('wire values map explicitly and preserve Apple Health states', () {
const expected = {
-1: HealthAuthorizationStatus.unavailable,
... ...