diff --git a/lib/common/permission/permissionRequestPage.dart b/lib/common/permission/permissionRequestPage.dart index 8a23fc2..3e53263 100644 --- a/lib/common/permission/permissionRequestPage.dart +++ b/lib/common/permission/permissionRequestPage.dart @@ -1,38 +1,24 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:wow_english/common/core/app_config_helper.dart'; +import 'package:wow_english/common/permission/permissionRequester.dart'; import '../../utils/log_util.dart'; -/// 权限检查及请求 -/// 外部可通过此方法来进行权限的检查和请求,将自动跳转到`PermissionRequestPage`页面。 -/// 传入 `Permission` 以及对应的权限名称 `permissionTypeStr`,如果有权限则返回 `Future true` -/// `isRequiredPermission` 如果为 `true`,则 "取消" 按钮将执行 "退出app" 的操作 -Future permissionCheckAndRequest( - BuildContext context, - Permission permission, - String permissionTypeStr, - {bool isRequiredPermission = false}) async { - if (!await permission.status.isGranted) { - await Navigator.of(context).push(PageRouteBuilder( - opaque: false, - pageBuilder: ((context, animation, secondaryAnimation) { - return PermissionRequestPage(permission, permissionTypeStr, - isRequiredPermission: isRequiredPermission); - }))); - } else { - return true; - } - return false; -} - +///带有隐私合规弹窗的透明权限申请页面,主要应对android各大应用市场对隐私权限申请需同步告知索取权限目的 class PermissionRequestPage extends StatefulWidget { - const PermissionRequestPage(this.permission, this.permissionTypeStr, + const PermissionRequestPage(this.permissions, + this.permissionNames, this.permissionDesc, {super.key, this.isRequiredPermission = false}); - final Permission permission; - final String permissionTypeStr; + final List permissions; + final List permissionNames; + final String permissionDesc; + + ///是否需要强制授予 final bool isRequiredPermission; @override @@ -49,13 +35,64 @@ class _PermissionRequestPageState extends State void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); + String permissionDesc = widget.permissionDesc; + String permissionStr = widget.permissionNames.join('、'); msgList = [ - "${widget.permissionTypeStr}功能需要获取您设备的${widget.permissionTypeStr}权限,否则可能无法正常工作。\n是否申请${widget.permissionTypeStr}权限?", - "${widget.permissionTypeStr}权限不全,是否重新申请权限?", - "没有${widget.permissionTypeStr}权限,您可以手动开启权限", - widget.isRequiredPermission ? "退出应用" : "取消" + "$permissionDesc,需要获取您设备的$permissionStr权限", + "你还没有开启$permissionStr权限,开启后即可$permissionDesc", + "未开启$permissionStr权限导致功能受限,您可以手动开启权限", + widget.isRequiredPermission ? "退出应用" : "以后再说" ]; - _checkPermission(widget.permission); + _handlePermission(widget.permissions); + } + + @override + Widget build(BuildContext context) { + double screenWidth = MediaQuery.of(context).size.width; + // PackageInfo packageInfo = await PackageInfo.fromPlatform(); + // String packageName = packageInfo.packageName; + return Scaffold( + backgroundColor: Colors.transparent, + body: Align( + alignment: Alignment.topCenter, + child: Container( + width: screenWidth / 2, + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(10.0), // 圆角半径 + boxShadow: const [ + BoxShadow( + color: Colors.black26, + blurRadius: 4.0, + offset: Offset(2, 2), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Text( + '${widget.permissionNames.join('、')}权限使用说明', + style: TextStyle( + color: Colors.black, + fontSize: 15.sp, + fontWeight: FontWeight.w500, + fontFamily: 'PingFangSC-Regular'), + textAlign: TextAlign.left, + ), + ), + 16.verticalSpace, + Text( + widget.permissionDesc, + style: const TextStyle(color: Colors.black54, + fontFamily: 'PingFangSC-Regular'), + textAlign: TextAlign.left, + ), + ], + )))); } @override @@ -63,46 +100,66 @@ class _PermissionRequestPageState extends State super.didChangeAppLifecycleState(state); Log.d("didChangeAppLifecycleState state=$state _isGoSetting=$_isGoSetting"); // 监听 app 从后台切回前台 - if (state == AppLifecycleState.resumed && _isGoSetting) { - _checkPermission(widget.permission); + if (state == AppLifecycleState.resumed && _isGoSetting && !_isDialogShowing) { + _handlePermission(widget.permissions); } } /// 校验权限 - void _checkPermission(Permission permission) async { - final PermissionStatus status = await permission.status; - _handlePermissionStatus(permission, status); + void _handlePermission(List permissions) async { + ///一个新待申请权限列表 + List intentPermissionList = []; + + ///遍历当前权限申请列表 + for (Permission permission in permissions) { + PermissionStatus status = await permission.status; + + ///如果不是允许状态就添加到新的申请列表中 + if (!status.isGranted) { + intentPermissionList.add(permission); + } + } + + if (intentPermissionList.isEmpty) { + _popPage(true); + } else { + _requestPermission(intentPermissionList); + } } - void _handlePermissionStatus(Permission permission, PermissionStatus status) { - Log.d('_handlePermissionStatus=$status permission=$permission'); - if (status.isGranted) { - _popPage(); + ///实际触发请求权限 + Future _requestPermission(List permissions) async { + Log.d('_requestPermission permissions=$permissions'); + MapEntry? statusEntry = + await requestPermissionsInner(permissions); + if (statusEntry == null) { + ///都手动同意授予了 + _popPage(true); return; } + Permission permission = statusEntry.key; + PermissionStatus status = statusEntry.value; + // 还未申请权限或之前拒绝了权限(在 iOS 上为首次申请权限,拒绝后将变为 `永久拒绝权限`) if (status.isDenied) { showAlert( - permission, msgList[0], msgList[3], _isGoSetting ? "前往系统设置" : "确定"); + permission, msgList[0], msgList[3], _isGoSetting ? "前往系统设置" : "继续"); } // 权限已被永久拒绝 + /// 在 Android 上:Android 11+ (API 30+):用户是否第二次拒绝权限。低于 Android 11 (API 30):用户是否拒绝访问请求的功能,并选择不再显示请求。 + /// 在 iOS 上:如果用户拒绝访问所请求的功能。 if (status.isPermanentlyDenied) { _isGoSetting = true; showAlert( - permission, msgList[2], msgList[3], _isGoSetting ? "前往系统设置" : "确定"); + permission, msgList[2], msgList[3], _isGoSetting ? "前往系统设置" : "继续"); } - // 拥有部分权限 - if (status.isLimited) { + // isLimited:拥有部分权限(受限,仅在 iOS (iOS14+) 上受支持) + // isRestricted:拥有部分权限,活动限制(例如,设置了家长///控件,仅在iOS以上受支持。(仅限 iOS) + if (status.isLimited || status.isRestricted) { if (Platform.isIOS || Platform.isMacOS) _isGoSetting = true; showAlert( - permission, msgList[1], msgList[3], _isGoSetting ? "前往系统设置" : "确定"); - } - // 拥有部分权限,活动限制(例如,设置了家长///控件,仅在iOS以上受支持。(仅限 iOS) - if (status.isRestricted) { - if (Platform.isIOS || Platform.isMacOS) _isGoSetting = true; - showAlert( - permission, msgList[1], msgList[3], _isGoSetting ? "前往系统设置" : "确定"); + permission, msgList[1], msgList[3], _isGoSetting ? "前往系统设置" : "继续"); } } @@ -126,7 +183,7 @@ class _PermissionRequestPageState extends State onPressed: () { widget.isRequiredPermission ? _quitApp() - : _popDialogAndPage(context); + : _popDialog(context, false); }), TextButton( child: Text(confirmMsg), @@ -134,24 +191,16 @@ class _PermissionRequestPageState extends State if (_isGoSetting) { openAppSettings(); } else { - _requestPermisson(permission); + _handlePermission(widget.permissions); } - _popDialog(context); + _popDialog(context, null); }) ], ); }).then((value) => { _isDialogShowing = false, - }); - } - - /// 申请权限 - void _requestPermisson(Permission permission) async { - // 申请权限 - PermissionStatus status = await permission.request(); - Log.d('requestPermisson权限检测=$status _isGoSetting=$_isGoSetting'); - // 再次校验 - _handlePermissionStatus(permission, status); + _popPage(value) + }); } @override @@ -160,29 +209,23 @@ class _PermissionRequestPageState extends State super.dispose(); } - @override - Widget build(BuildContext context) { - return Container(); - } - /// 退出应用程序 void _quitApp() { AppConfigHelper.exitApp(); } - /// 关闭整个权限申请页面 - void _popDialogAndPage(BuildContext dialogContext) { - _popDialog(dialogContext); - _popPage(); - } - /// 关闭弹窗 - void _popDialog(BuildContext dialogContext) { + /// isAllGranted为null的话跳到系统设置页 + void _popDialog(BuildContext dialogContext, bool? isAllGranted) { Navigator.of(dialogContext).pop(); + _popPage(isAllGranted); } - /// 关闭透明页面 - void _popPage() { - Navigator.of(context).pop(); + /// 关闭权限申请透明页面 + /// isAllGranted 所有权限都授予,为空不关闭 + void _popPage(bool? isAllGranted) { + if (isAllGranted != null) { + Navigator.of(context).pop(isAllGranted); + } } } diff --git a/lib/common/permission/permissionRequester.dart b/lib/common/permission/permissionRequester.dart new file mode 100644 index 0000000..e65c7f1 --- /dev/null +++ b/lib/common/permission/permissionRequester.dart @@ -0,0 +1,64 @@ +import 'package:flutter/cupertino.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:wow_english/common/permission/permissionRequestPage.dart'; + +/// 带隐私合规功能的权限检查及请求入口 +/// 外部可通过此方法来进行权限的检查和请求,将自动跳转到`PermissionRequestPage`页面。 +/// 传入 `Permission` 以及对应的(中文)权限名称 `permissionName` +/// permissionDesc:设备权限使用说明(描述) +/// isRequiredPermission:是否强制要求 如果为 `true`,则 "取消" 按钮将执行 "退出app" 的操作 +/// return 如果有权限则返回 `Future` +Future requestPermission( + BuildContext context, + Permission permission, + String permissionName, + String permissionDesc, + {bool isRequiredPermission = false}) async { + return requestPermissions(context, [permission], [permissionName], permissionDesc, + isRequiredPermission: isRequiredPermission); +} + + +Future requestPermissions( + BuildContext context, + List permissions, + List permissionNames, + String permissionDesc, + {bool isRequiredPermission = false}) async { + + // List statuses = await Future.wait( + // permissions.map((permission) => permission.status), + // ); + // bool allGranted = statuses.every((status) => status.isGranted); + + bool allGranted = await isPermissionsGranted(permissions); + if (allGranted) { + return true; + } else { + return await Navigator.of(context).push(PageRouteBuilder( + opaque: false, + pageBuilder: ((context, animation, secondaryAnimation) { + return PermissionRequestPage(permissions, permissionNames, permissionDesc, + isRequiredPermission: isRequiredPermission); + }))); + } +} + +///(实际请求前)判断权限数组是否都授予 +Future isPermissionsGranted(List permissions) async { + // 使用 every 直接检查权限状态 + return await Future.wait(permissions.map((permission) async { + return await permission.status.isGranted; + })).then((statuses) => statuses.every((status) => status)); +} + +///请求权限 +Future?> requestPermissionsInner(List permissionList) async { + Map statusesMap = await permissionList.request(); + for (var entry in statusesMap.entries) { + if (!entry.value.isGranted) { + return entry; + } + } + return null; +} \ No newline at end of file diff --git a/lib/pages/practice/bloc/topic_picture_bloc.dart b/lib/pages/practice/bloc/topic_picture_bloc.dart index 906941b..20aa694 100644 --- a/lib/pages/practice/bloc/topic_picture_bloc.dart +++ b/lib/pages/practice/bloc/topic_picture_bloc.dart @@ -15,7 +15,7 @@ import 'package:wow_english/pages/section/subsection/base_section/state.dart'; import 'package:wow_english/utils/loading.dart'; import 'package:wow_english/utils/toast_util.dart'; -import '../../../common/permission/permissionRequestPage.dart'; +import '../../../common/permission/permissionRequester.dart'; import '../../../route/route.dart'; part 'topic_picture_event.dart'; @@ -258,7 +258,7 @@ class TopicPictureBloc await audioPlayer.stop(); // 调用封装好的权限检查和请求方法 bool result = - await permissionCheckAndRequest(context, Permission.microphone, "录音"); + await requestPermission(context, Permission.microphone, "录音", "用于开启录音,识别您的开口作答并给出反馈"); if (result) { methodChannel.invokeMethod('startVoice', { 'word': event.testWord, diff --git a/lib/pages/reading/bloc/reading_bloc.dart b/lib/pages/reading/bloc/reading_bloc.dart index e66fbbb..fcbd9a3 100644 --- a/lib/pages/reading/bloc/reading_bloc.dart +++ b/lib/pages/reading/bloc/reading_bloc.dart @@ -11,6 +11,7 @@ import 'package:wow_english/pages/section/subsection/base_section/event.dart'; import 'package:wow_english/pages/section/subsection/base_section/state.dart'; import '../../../common/core/user_util.dart'; +import '../../../common/permission/permissionRequester.dart'; import '../../../common/request/dao/listen_dao.dart'; import '../../../common/request/exception.dart'; import '../../../models/course_process_entity.dart'; @@ -336,7 +337,7 @@ class ReadingPageBloc void startRecord(String content) async { // 调用封装好的权限检查和请求方法 bool result = - await permissionCheckAndRequest(context, Permission.microphone, "录音"); + await requestPermission(context, Permission.microphone, "录音", "用于开启录音,识别您的开口作答并给出反馈"); if (result) { methodChannel.invokeMethod('startVoice', { 'word': content, diff --git a/lib/pages/user/modify/modify_user_avatar_page.dart b/lib/pages/user/modify/modify_user_avatar_page.dart index b697a12..31b7ffb 100644 --- a/lib/pages/user/modify/modify_user_avatar_page.dart +++ b/lib/pages/user/modify/modify_user_avatar_page.dart @@ -22,7 +22,7 @@ class ModifyUserAvatarPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => UserAvatarBloc(), + create: (context) => UserAvatarBloc(context), child: _ModifyUserAvatarPage(pageType: pageType), ); } diff --git a/lib/pages/user/modify/user_avatar_bloc/user_avatar_bloc.dart b/lib/pages/user/modify/user_avatar_bloc/user_avatar_bloc.dart index 7cc3100..b7c8929 100644 --- a/lib/pages/user/modify/user_avatar_bloc/user_avatar_bloc.dart +++ b/lib/pages/user/modify/user_avatar_bloc/user_avatar_bloc.dart @@ -14,6 +14,8 @@ import 'package:wow_english/utils/aliyun_oss_util.dart'; import 'package:wow_english/utils/log_util.dart'; import 'package:wow_english/utils/toast_util.dart'; +import '../../../../common/permission/permissionRequester.dart'; + part 'user_avatar_event.dart'; part 'user_avatar_state.dart'; @@ -32,7 +34,9 @@ class UserAvatarBloc extends Bloc { final ImagePicker picker = ImagePicker(); - UserAvatarBloc() : super(UserAvatarInitial()) { + final BuildContext context; + + UserAvatarBloc(this.context) : super(UserAvatarInitial()) { on(_getImageFromPhoto); on(_getImageFromCamera); on(_changeUserEnterAppState); @@ -56,7 +60,8 @@ class UserAvatarBloc extends Bloc { } else { permission = Permission.photos; } - await getPermissionStatus(permission).then((value) async { + await requestPermission(context, permission, "文件读取", "用于在更换头像场景下从相册中选取图片等文件").then((value) async { + // await getPermissionStatus(permission).then((value) async { if (!value) { debugPrint('失败$value'); return; @@ -75,11 +80,8 @@ class UserAvatarBloc extends Bloc { } void _getImageFromCamera(GetImageFromCameraEvent event, Emitter emitter) async { - await getPermissionStatus(Permission.camera).then((value) async { - if (!value) { - debugPrint('失败$value'); - return; - } + bool result = await requestPermission(context, Permission.camera, "拍照", "用于在更换头像场景下调用相机拍照"); + if (result) { _file = await picker.pickImage(source: ImageSource.camera); EasyLoading.show(); try { @@ -90,7 +92,23 @@ class UserAvatarBloc extends Bloc { showToast('上传头像失败: $e'); } EasyLoading.dismiss(); - }); + } + // await getPermissionStatus(Permission.camera).then((value) async { + // if (!value) { + // debugPrint('失败$value'); + // return; + // } + // _file = await picker.pickImage(source: ImageSource.camera); + // EasyLoading.show(); + // try { + // final urlStr = await _uploadAvatar(_file!.path); + // emitter(ChangeImageState(urlStr)); + // } catch (e) { + // Log.e('上传头像失败:$e'); + // showToast('上传头像失败: $e'); + // } + // EasyLoading.dismiss(); + // }); } void _changeUserEnterAppState(ChangeUserEnterAppStateEvent event, Emitter emitter) async {