Commit 698ce9fe75b69b0d12296d6f693ad9f6112c727c

Authored by biao
2 parents b1869cf8 ba96a4bc

Merge branch 'master' into dev_song_master

lib/common/permission/permissionRequestPage.dart
  1 +import 'dart:async';
1 2 import 'dart:io';
2 3 import 'package:flutter/material.dart';
  4 +import 'package:flutter_screenutil/flutter_screenutil.dart';
3 5 import 'package:permission_handler/permission_handler.dart';
4 6 import 'package:wow_english/common/core/app_config_helper.dart';
  7 +import 'package:wow_english/common/permission/permissionRequester.dart';
5 8  
6 9 import '../../utils/log_util.dart';
7 10  
8   -/// 权限检查及请求
9   -/// 外部可通过此方法来进行权限的检查和请求,将自动跳转到`PermissionRequestPage`页面。
10   -/// 传入 `Permission` 以及对应的权限名称 `permissionTypeStr`,如果有权限则返回 `Future true`
11   -/// `isRequiredPermission` 如果为 `true`,则 "取消" 按钮将执行 "退出app" 的操作
12   -Future<bool> permissionCheckAndRequest(
13   - BuildContext context,
14   - Permission permission,
15   - String permissionTypeStr,
16   - {bool isRequiredPermission = false}) async {
17   - if (!await permission.status.isGranted) {
18   - await Navigator.of(context).push(PageRouteBuilder(
19   - opaque: false,
20   - pageBuilder: ((context, animation, secondaryAnimation) {
21   - return PermissionRequestPage(permission, permissionTypeStr,
22   - isRequiredPermission: isRequiredPermission);
23   - })));
24   - } else {
25   - return true;
26   - }
27   - return false;
28   -}
29   -
  11 +///带有隐私合规弹窗的透明权限申请页面,主要应对android各大应用市场对隐私权限申请需同步告知索取权限目的
30 12 class PermissionRequestPage extends StatefulWidget {
31   - const PermissionRequestPage(this.permission, this.permissionTypeStr,
  13 + const PermissionRequestPage(this.permissions,
  14 + this.permissionNames, this.permissionDesc,
32 15 {super.key, this.isRequiredPermission = false});
33 16  
34   - final Permission permission;
35   - final String permissionTypeStr;
  17 + final List<Permission> permissions;
  18 + final List<String> permissionNames;
  19 + final String permissionDesc;
  20 +
  21 + ///是否需要强制授予
36 22 final bool isRequiredPermission;
37 23  
38 24 @override
... ... @@ -49,13 +35,64 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
49 35 void initState() {
50 36 super.initState();
51 37 WidgetsBinding.instance.addObserver(this);
  38 + String permissionDesc = widget.permissionDesc;
  39 + String permissionStr = widget.permissionNames.join('、');
52 40 msgList = [
53   - "${widget.permissionTypeStr}功能需要获取您设备的${widget.permissionTypeStr}权限,否则可能无法正常工作。\n是否申请${widget.permissionTypeStr}权限?",
54   - "${widget.permissionTypeStr}权限不全,是否重新申请权限?",
55   - "没有${widget.permissionTypeStr}权限,您可以手动开启权限",
56   - widget.isRequiredPermission ? "退出应用" : "取消"
  41 + "$permissionDesc,需要获取您设备的$permissionStr权限",
  42 + "你还没有开启$permissionStr权限,开启后即可$permissionDesc",
  43 + "未开启$permissionStr权限导致功能受限,您可以手动开启权限",
  44 + widget.isRequiredPermission ? "退出应用" : "以后再说"
57 45 ];
58   - _checkPermission(widget.permission);
  46 + _handlePermission(widget.permissions);
  47 + }
  48 +
  49 + @override
  50 + Widget build(BuildContext context) {
  51 + double screenWidth = MediaQuery.of(context).size.width;
  52 + // PackageInfo packageInfo = await PackageInfo.fromPlatform();
  53 + // String packageName = packageInfo.packageName;
  54 + return Scaffold(
  55 + backgroundColor: Colors.transparent,
  56 + body: Align(
  57 + alignment: Alignment.topCenter,
  58 + child: Container(
  59 + width: screenWidth / 2,
  60 + padding: const EdgeInsets.all(16.0),
  61 + decoration: BoxDecoration(
  62 + color: Colors.white,
  63 + borderRadius: BorderRadius.circular(10.0), // 圆角半径
  64 + boxShadow: const [
  65 + BoxShadow(
  66 + color: Colors.black26,
  67 + blurRadius: 4.0,
  68 + offset: Offset(2, 2),
  69 + ),
  70 + ],
  71 + ),
  72 + child: Column(
  73 + mainAxisSize: MainAxisSize.min,
  74 + crossAxisAlignment: CrossAxisAlignment.start,
  75 + children: [
  76 + Center(
  77 + child: Text(
  78 + '${widget.permissionNames.join('、')}权限使用说明',
  79 + style: TextStyle(
  80 + color: Colors.black,
  81 + fontSize: 15.sp,
  82 + fontWeight: FontWeight.w500,
  83 + fontFamily: 'PingFangSC-Regular'),
  84 + textAlign: TextAlign.left,
  85 + ),
  86 + ),
  87 + 16.verticalSpace,
  88 + Text(
  89 + widget.permissionDesc,
  90 + style: const TextStyle(color: Colors.black54,
  91 + fontFamily: 'PingFangSC-Regular'),
  92 + textAlign: TextAlign.left,
  93 + ),
  94 + ],
  95 + ))));
59 96 }
60 97  
61 98 @override
... ... @@ -63,46 +100,66 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
63 100 super.didChangeAppLifecycleState(state);
64 101 Log.d("didChangeAppLifecycleState state=$state _isGoSetting=$_isGoSetting");
65 102 // 监听 app 从后台切回前台
66   - if (state == AppLifecycleState.resumed && _isGoSetting) {
67   - _checkPermission(widget.permission);
  103 + if (state == AppLifecycleState.resumed && _isGoSetting && !_isDialogShowing) {
  104 + _handlePermission(widget.permissions);
68 105 }
69 106 }
70 107  
71 108 /// 校验权限
72   - void _checkPermission(Permission permission) async {
73   - final PermissionStatus status = await permission.status;
74   - _handlePermissionStatus(permission, status);
  109 + void _handlePermission(List<Permission> permissions) async {
  110 + ///一个新待申请权限列表
  111 + List<Permission> intentPermissionList = [];
  112 +
  113 + ///遍历当前权限申请列表
  114 + for (Permission permission in permissions) {
  115 + PermissionStatus status = await permission.status;
  116 +
  117 + ///如果不是允许状态就添加到新的申请列表中
  118 + if (!status.isGranted) {
  119 + intentPermissionList.add(permission);
  120 + }
  121 + }
  122 +
  123 + if (intentPermissionList.isEmpty) {
  124 + _popPage(true);
  125 + } else {
  126 + _requestPermission(intentPermissionList);
  127 + }
75 128 }
76 129  
77   - void _handlePermissionStatus(Permission permission, PermissionStatus status) {
78   - Log.d('_handlePermissionStatus=$status permission=$permission');
79   - if (status.isGranted) {
80   - _popPage();
  130 + ///实际触发请求权限
  131 + Future<void> _requestPermission(List<Permission> permissions) async {
  132 + Log.d('_requestPermission permissions=$permissions');
  133 + MapEntry<Permission, PermissionStatus>? statusEntry =
  134 + await requestPermissionsInner(permissions);
  135 + if (statusEntry == null) {
  136 + ///都手动同意授予了
  137 + _popPage(true);
81 138 return;
82 139 }
83 140  
  141 + Permission permission = statusEntry.key;
  142 + PermissionStatus status = statusEntry.value;
  143 +
84 144 // 还未申请权限或之前拒绝了权限(在 iOS 上为首次申请权限,拒绝后将变为 `永久拒绝权限`)
85 145 if (status.isDenied) {
86 146 showAlert(
87   - permission, msgList[0], msgList[3], _isGoSetting ? "前往系统设置" : "确定");
  147 + permission, msgList[0], msgList[3], _isGoSetting ? "前往系统设置" : "继续");
88 148 }
89 149 // 权限已被永久拒绝
  150 + /// 在 Android 上:Android 11+ (API 30+):用户是否第二次拒绝权限。低于 Android 11 (API 30):用户是否拒绝访问请求的功能,并选择不再显示请求。
  151 + /// 在 iOS 上:如果用户拒绝访问所请求的功能。
90 152 if (status.isPermanentlyDenied) {
91 153 _isGoSetting = true;
92 154 showAlert(
93   - permission, msgList[2], msgList[3], _isGoSetting ? "前往系统设置" : "确定");
  155 + permission, msgList[2], msgList[3], _isGoSetting ? "前往系统设置" : "继续");
94 156 }
95   - // 拥有部分权限
96   - if (status.isLimited) {
  157 + // isLimited:拥有部分权限(受限,仅在 iOS (iOS14+) 上受支持)
  158 + // isRestricted:拥有部分权限,活动限制(例如,设置了家长///控件,仅在iOS以上受支持。(仅限 iOS)
  159 + if (status.isLimited || status.isRestricted) {
97 160 if (Platform.isIOS || Platform.isMacOS) _isGoSetting = true;
98 161 showAlert(
99   - permission, msgList[1], msgList[3], _isGoSetting ? "前往系统设置" : "确定");
100   - }
101   - // 拥有部分权限,活动限制(例如,设置了家长///控件,仅在iOS以上受支持。(仅限 iOS)
102   - if (status.isRestricted) {
103   - if (Platform.isIOS || Platform.isMacOS) _isGoSetting = true;
104   - showAlert(
105   - permission, msgList[1], msgList[3], _isGoSetting ? "前往系统设置" : "确定");
  162 + permission, msgList[1], msgList[3], _isGoSetting ? "前往系统设置" : "继续");
106 163 }
107 164 }
108 165  
... ... @@ -126,7 +183,7 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
126 183 onPressed: () {
127 184 widget.isRequiredPermission
128 185 ? _quitApp()
129   - : _popDialogAndPage(context);
  186 + : _popDialog(context, false);
130 187 }),
131 188 TextButton(
132 189 child: Text(confirmMsg),
... ... @@ -134,24 +191,16 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
134 191 if (_isGoSetting) {
135 192 openAppSettings();
136 193 } else {
137   - _requestPermisson(permission);
  194 + _handlePermission(widget.permissions);
138 195 }
139   - _popDialog(context);
  196 + _popDialog(context, null);
140 197 })
141 198 ],
142 199 );
143 200 }).then((value) => {
144 201 _isDialogShowing = false,
145   - });
146   - }
147   -
148   - /// 申请权限
149   - void _requestPermisson(Permission permission) async {
150   - // 申请权限
151   - PermissionStatus status = await permission.request();
152   - Log.d('requestPermisson权限检测=$status _isGoSetting=$_isGoSetting');
153   - // 再次校验
154   - _handlePermissionStatus(permission, status);
  202 + _popPage(value)
  203 + });
155 204 }
156 205  
157 206 @override
... ... @@ -160,29 +209,23 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
160 209 super.dispose();
161 210 }
162 211  
163   - @override
164   - Widget build(BuildContext context) {
165   - return Container();
166   - }
167   -
168 212 /// 退出应用程序
169 213 void _quitApp() {
170 214 AppConfigHelper.exitApp();
171 215 }
172 216  
173   - /// 关闭整个权限申请页面
174   - void _popDialogAndPage(BuildContext dialogContext) {
175   - _popDialog(dialogContext);
176   - _popPage();
177   - }
178   -
179 217 /// 关闭弹窗
180   - void _popDialog(BuildContext dialogContext) {
  218 + /// isAllGranted为null的话跳到系统设置页
  219 + void _popDialog(BuildContext dialogContext, bool? isAllGranted) {
181 220 Navigator.of(dialogContext).pop();
  221 + _popPage(isAllGranted);
182 222 }
183 223  
184   - /// 关闭透明页面
185   - void _popPage() {
186   - Navigator.of(context).pop();
  224 + /// 关闭权限申请透明页面
  225 + /// isAllGranted 所有权限都授予,为空不关闭
  226 + void _popPage(bool? isAllGranted) {
  227 + if (isAllGranted != null) {
  228 + Navigator.of(context).pop(isAllGranted);
  229 + }
187 230 }
188 231 }
... ...
lib/common/permission/permissionRequester.dart 0 → 100644
  1 +import 'package:flutter/cupertino.dart';
  2 +import 'package:permission_handler/permission_handler.dart';
  3 +import 'package:wow_english/common/permission/permissionRequestPage.dart';
  4 +
  5 +/// 带隐私合规功能的权限检查及请求入口
  6 +/// 外部可通过此方法来进行权限的检查和请求,将自动跳转到`PermissionRequestPage`页面。
  7 +/// 传入 `Permission` 以及对应的(中文)权限名称 `permissionName`
  8 +/// permissionDesc:设备权限使用说明(描述)
  9 +/// isRequiredPermission:是否强制要求 如果为 `true`,则 "取消" 按钮将执行 "退出app" 的操作
  10 +/// return 如果有权限则返回 `Future<true>`
  11 +Future<bool> requestPermission(
  12 + BuildContext context,
  13 + Permission permission,
  14 + String permissionName,
  15 + String permissionDesc,
  16 + {bool isRequiredPermission = false}) async {
  17 + return requestPermissions(context, [permission], [permissionName], permissionDesc,
  18 + isRequiredPermission: isRequiredPermission);
  19 +}
  20 +
  21 +
  22 +Future<bool> requestPermissions(
  23 + BuildContext context,
  24 + List<Permission> permissions,
  25 + List<String> permissionNames,
  26 + String permissionDesc,
  27 + {bool isRequiredPermission = false}) async {
  28 +
  29 + // List<PermissionStatus> statuses = await Future.wait(
  30 + // permissions.map((permission) => permission.status),
  31 + // );
  32 + // bool allGranted = statuses.every((status) => status.isGranted);
  33 +
  34 + bool allGranted = await isPermissionsGranted(permissions);
  35 + if (allGranted) {
  36 + return true;
  37 + } else {
  38 + return await Navigator.of(context).push(PageRouteBuilder(
  39 + opaque: false,
  40 + pageBuilder: ((context, animation, secondaryAnimation) {
  41 + return PermissionRequestPage(permissions, permissionNames, permissionDesc,
  42 + isRequiredPermission: isRequiredPermission);
  43 + })));
  44 + }
  45 +}
  46 +
  47 +///(实际请求前)判断权限数组是否都授予
  48 +Future<bool> isPermissionsGranted(List<Permission> permissions) async {
  49 + // 使用 every 直接检查权限状态
  50 + return await Future.wait(permissions.map((permission) async {
  51 + return await permission.status.isGranted;
  52 + })).then((statuses) => statuses.every((status) => status));
  53 +}
  54 +
  55 +///请求权限
  56 +Future<MapEntry<Permission, PermissionStatus>?> requestPermissionsInner(List<Permission> permissionList) async {
  57 + Map<Permission, PermissionStatus> statusesMap = await permissionList.request();
  58 + for (var entry in statusesMap.entries) {
  59 + if (!entry.value.isGranted) {
  60 + return entry;
  61 + }
  62 + }
  63 + return null;
  64 +}
0 65 \ No newline at end of file
... ...
lib/pages/practice/bloc/topic_picture_bloc.dart
... ... @@ -15,7 +15,7 @@ import &#39;package:wow_english/pages/section/subsection/base_section/state.dart&#39;;
15 15 import 'package:wow_english/utils/loading.dart';
16 16 import 'package:wow_english/utils/toast_util.dart';
17 17  
18   -import '../../../common/permission/permissionRequestPage.dart';
  18 +import '../../../common/permission/permissionRequester.dart';
19 19 import '../../../route/route.dart';
20 20  
21 21 part 'topic_picture_event.dart';
... ... @@ -258,7 +258,7 @@ class TopicPictureBloc
258 258 await audioPlayer.stop();
259 259 // 调用封装好的权限检查和请求方法
260 260 bool result =
261   - await permissionCheckAndRequest(context, Permission.microphone, "录音");
  261 + await requestPermission(context, Permission.microphone, "录音", "用于开启录音,识别您的开口作答并给出反馈");
262 262 if (result) {
263 263 methodChannel.invokeMethod('startVoice', {
264 264 'word': event.testWord,
... ...
lib/pages/reading/bloc/reading_bloc.dart
... ... @@ -11,6 +11,7 @@ import &#39;package:wow_english/pages/section/subsection/base_section/event.dart&#39;;
11 11 import 'package:wow_english/pages/section/subsection/base_section/state.dart';
12 12  
13 13 import '../../../common/core/user_util.dart';
  14 +import '../../../common/permission/permissionRequester.dart';
14 15 import '../../../common/request/dao/listen_dao.dart';
15 16 import '../../../common/request/exception.dart';
16 17 import '../../../models/course_process_entity.dart';
... ... @@ -336,7 +337,7 @@ class ReadingPageBloc
336 337 void startRecord(String content) async {
337 338 // 调用封装好的权限检查和请求方法
338 339 bool result =
339   - await permissionCheckAndRequest(context, Permission.microphone, "录音");
  340 + await requestPermission(context, Permission.microphone, "录音", "用于开启录音,识别您的开口作答并给出反馈");
340 341 if (result) {
341 342 methodChannel.invokeMethod('startVoice', {
342 343 'word': content,
... ...
lib/pages/user/modify/modify_user_avatar_page.dart
... ... @@ -22,7 +22,7 @@ class ModifyUserAvatarPage extends StatelessWidget {
22 22 @override
23 23 Widget build(BuildContext context) {
24 24 return BlocProvider(
25   - create: (context) => UserAvatarBloc(),
  25 + create: (context) => UserAvatarBloc(context),
26 26 child: _ModifyUserAvatarPage(pageType: pageType),
27 27 );
28 28 }
... ...
lib/pages/user/modify/user_avatar_bloc/user_avatar_bloc.dart
... ... @@ -14,6 +14,8 @@ import &#39;package:wow_english/utils/aliyun_oss_util.dart&#39;;
14 14 import 'package:wow_english/utils/log_util.dart';
15 15 import 'package:wow_english/utils/toast_util.dart';
16 16  
  17 +import '../../../../common/permission/permissionRequester.dart';
  18 +
17 19 part 'user_avatar_event.dart';
18 20 part 'user_avatar_state.dart';
19 21  
... ... @@ -32,7 +34,9 @@ class UserAvatarBloc extends Bloc&lt;UserAvatarEvent, UserAvatarState&gt; {
32 34  
33 35 final ImagePicker picker = ImagePicker();
34 36  
35   - UserAvatarBloc() : super(UserAvatarInitial()) {
  37 + final BuildContext context;
  38 +
  39 + UserAvatarBloc(this.context) : super(UserAvatarInitial()) {
36 40 on<GetImageFromPhotoEvent>(_getImageFromPhoto);
37 41 on<GetImageFromCameraEvent>(_getImageFromCamera);
38 42 on<ChangeUserEnterAppStateEvent>(_changeUserEnterAppState);
... ... @@ -56,7 +60,8 @@ class UserAvatarBloc extends Bloc&lt;UserAvatarEvent, UserAvatarState&gt; {
56 60 } else {
57 61 permission = Permission.photos;
58 62 }
59   - await getPermissionStatus(permission).then((value) async {
  63 + await requestPermission(context, permission, "文件读取", "用于在更换头像场景下从相册中选取图片等文件").then((value) async {
  64 + // await getPermissionStatus(permission).then((value) async {
60 65 if (!value) {
61 66 debugPrint('失败$value');
62 67 return;
... ... @@ -75,11 +80,8 @@ class UserAvatarBloc extends Bloc&lt;UserAvatarEvent, UserAvatarState&gt; {
75 80 }
76 81  
77 82 void _getImageFromCamera(GetImageFromCameraEvent event, Emitter<UserAvatarState> emitter) async {
78   - await getPermissionStatus(Permission.camera).then((value) async {
79   - if (!value) {
80   - debugPrint('失败$value');
81   - return;
82   - }
  83 + bool result = await requestPermission(context, Permission.camera, "拍照", "用于在更换头像场景下调用相机拍照");
  84 + if (result) {
83 85 _file = await picker.pickImage(source: ImageSource.camera);
84 86 EasyLoading.show();
85 87 try {
... ... @@ -90,7 +92,23 @@ class UserAvatarBloc extends Bloc&lt;UserAvatarEvent, UserAvatarState&gt; {
90 92 showToast('上传头像失败: $e');
91 93 }
92 94 EasyLoading.dismiss();
93   - });
  95 + }
  96 + // await getPermissionStatus(Permission.camera).then((value) async {
  97 + // if (!value) {
  98 + // debugPrint('失败$value');
  99 + // return;
  100 + // }
  101 + // _file = await picker.pickImage(source: ImageSource.camera);
  102 + // EasyLoading.show();
  103 + // try {
  104 + // final urlStr = await _uploadAvatar(_file!.path);
  105 + // emitter(ChangeImageState(urlStr));
  106 + // } catch (e) {
  107 + // Log.e('上传头像失败:$e');
  108 + // showToast('上传头像失败: $e');
  109 + // }
  110 + // EasyLoading.dismiss();
  111 + // });
94 112 }
95 113  
96 114 void _changeUserEnterAppState(ChangeUserEnterAppStateEvent event, Emitter<UserAvatarState> emitter) async {
... ...