Commit e811f16468de569f388a6e2e88e8ddea9c680a0f

Authored by 吴启风
1 parent 1daca20d

feat:权限申请页面增加隐私合规弹窗(android)

lib/common/permission/permissionRequestPage.dart
1 1 import 'dart:async';
2 2 import 'dart:io';
3 3 import 'package:flutter/material.dart';
4   -import 'package:package_info_plus/package_info_plus.dart';
  4 +import 'package:flutter_screenutil/flutter_screenutil.dart';
5 5 import 'package:permission_handler/permission_handler.dart';
6 6 import 'package:wow_english/common/core/app_config_helper.dart';
  7 +import 'package:wow_english/common/permission/permissionRequester.dart';
7 8  
8 9 import '../../utils/log_util.dart';
9 10  
10   -/// 带隐私合规功能的权限检查及请求
11   -/// 外部可通过此方法来进行权限的检查和请求,将自动跳转到`PermissionRequestPage`页面。
12   -/// 传入 `Permission` 以及对应的权限名称 `permissionTypeStr`,如果有权限则返回 `Future true`
13   -/// `isRequiredPermission` 如果为 `true`,则 "取消" 按钮将执行 "退出app" 的操作
14   -Future<bool> permissionCheckAndRequests(
15   - BuildContext context,
16   - List<Permission> permissions,
17   - List<String> permissionTypeStrs,
18   - {bool isRequiredPermission = false}) async {
19   -
20   - // List<PermissionStatus> statuses = await Future.wait(
21   - // permissions.map((permission) => permission.status),
22   - // );
23   - // bool allGranted = statuses.every((status) => status.isGranted);
24   -
25   - bool allGranted = await isPermissionsGranted(permissions);
26   - if (allGranted) {
27   - return true;
28   - } else {
29   - return await Navigator.of(context).push(PageRouteBuilder(
30   - opaque: false,
31   - pageBuilder: ((context, animation, secondaryAnimation) {
32   - return PermissionRequestPage(permissions, permissionTypeStrs,
33   - isRequiredPermission: isRequiredPermission);
34   - })));
35   - }
36   -}
37   -
38   -Future<bool> permissionCheckAndRequest(
39   - BuildContext context,
40   - Permission permission,
41   - String permissionTypeStr,
42   - {bool isRequiredPermission = false}) async {
43   - return permissionCheckAndRequests(context, [permission], [permissionTypeStr],
44   - isRequiredPermission: isRequiredPermission);
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>?> requestPermissions(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   -}
65   -
66 11 class PermissionRequestPage extends StatefulWidget {
67   - const PermissionRequestPage(this.permissions, this.permissionTypeStrs,
  12 + const PermissionRequestPage(this.permissions,
  13 + this.permissionNames, this.permissionDesc,
68 14 {super.key, this.isRequiredPermission = false});
69 15  
70 16 final List<Permission> permissions;
71   - final List<String> permissionTypeStrs;
  17 + final List<String> permissionNames;
  18 + final String permissionDesc;
  19 +
72 20 ///是否需要强制授予
73 21 final bool isRequiredPermission;
74 22  
... ... @@ -86,22 +34,72 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
86 34 void initState() {
87 35 super.initState();
88 36 WidgetsBinding.instance.addObserver(this);
89   - String permissionTypeStrs = widget.permissionTypeStrs.join('、');
  37 + String permissionDesc = widget.permissionDesc;
  38 + String permissionStr = widget.permissionNames.join('、');
90 39 msgList = [
91   - "Wow English需要获取您设备的$permissionTypeStrs权限,否则可能无法正常工作。\n是否同意授予?",
92   - "$permissionTypeStrs权限不全,是否重新申请权限?",
93   - "没有$permissionTypeStrs权限,您可以手动开启权限",
94   - widget.isRequiredPermission ? "退出应用" : "取消"
  40 + "$permissionDesc,需要获取您设备的$permissionStr权限",
  41 + "你还没有开启$permissionStr权限,开启后即可$permissionDesc",
  42 + "未开启$permissionStr权限导致功能受限,您可以手动开启权限",
  43 + widget.isRequiredPermission ? "退出应用" : "以后再说"
95 44 ];
96 45 _handlePermission(widget.permissions);
97 46 }
98 47  
99 48 @override
  49 + Widget build(BuildContext context) {
  50 + double screenWidth = MediaQuery.of(context).size.width;
  51 + // PackageInfo packageInfo = await PackageInfo.fromPlatform();
  52 + // String packageName = packageInfo.packageName;
  53 + return Scaffold(
  54 + backgroundColor: Colors.transparent,
  55 + body: Align(
  56 + alignment: Alignment.topCenter,
  57 + child: Container(
  58 + width: screenWidth / 2,
  59 + padding: const EdgeInsets.all(16.0),
  60 + decoration: BoxDecoration(
  61 + color: Colors.white,
  62 + borderRadius: BorderRadius.circular(10.0), // 圆角半径
  63 + boxShadow: const [
  64 + BoxShadow(
  65 + color: Colors.black26,
  66 + blurRadius: 4.0,
  67 + offset: Offset(2, 2),
  68 + ),
  69 + ],
  70 + ),
  71 + child: Column(
  72 + mainAxisSize: MainAxisSize.min,
  73 + crossAxisAlignment: CrossAxisAlignment.start,
  74 + children: [
  75 + Center(
  76 + child: Text(
  77 + '${widget.permissionNames.join('、')}权限使用说明',
  78 + style: TextStyle(
  79 + color: Colors.black,
  80 + fontSize: 15.sp,
  81 + fontWeight: FontWeight.w500,
  82 + fontFamily: 'PingFangSC-Regular'),
  83 + textAlign: TextAlign.left,
  84 + ),
  85 + ),
  86 + 16.verticalSpace,
  87 + Text(
  88 + widget.permissionDesc,
  89 + style: const TextStyle(color: Colors.black54,
  90 + fontFamily: 'PingFangSC-Regular'),
  91 + textAlign: TextAlign.left,
  92 + ),
  93 + ],
  94 + ))));
  95 + }
  96 +
  97 + @override
100 98 void didChangeAppLifecycleState(AppLifecycleState state) {
101 99 super.didChangeAppLifecycleState(state);
102 100 Log.d("didChangeAppLifecycleState state=$state _isGoSetting=$_isGoSetting");
103 101 // 监听 app 从后台切回前台
104   - if (state == AppLifecycleState.resumed && _isGoSetting) {
  102 + if (state == AppLifecycleState.resumed && _isGoSetting && !_isDialogShowing) {
105 103 _handlePermission(widget.permissions);
106 104 }
107 105 }
... ... @@ -131,12 +129,8 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
131 129 ///实际触发请求权限
132 130 Future<void> _requestPermission(List<Permission> permissions) async {
133 131 Log.d('_requestPermission permissions=$permissions');
134   - if (await isPermissionsGranted(permissions)) {
135   - _popPage(true);
136   - return;
137   - }
138   -
139   - MapEntry<Permission, PermissionStatus>? statusEntry = await requestPermissions(permissions);
  132 + MapEntry<Permission, PermissionStatus>? statusEntry =
  133 + await requestPermissionsInner(permissions);
140 134 if (statusEntry == null) {
141 135 ///都手动同意授予了
142 136 _popPage(true);
... ... @@ -145,10 +139,11 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
145 139  
146 140 Permission permission = statusEntry.key;
147 141 PermissionStatus status = statusEntry.value;
  142 +
148 143 // 还未申请权限或之前拒绝了权限(在 iOS 上为首次申请权限,拒绝后将变为 `永久拒绝权限`)
149 144 if (status.isDenied) {
150 145 showAlert(
151   - permission, msgList[0], msgList[3], _isGoSetting ? "前往系统设置" : "确定");
  146 + permission, msgList[0], msgList[3], _isGoSetting ? "前往系统设置" : "继续");
152 147 }
153 148 // 权限已被永久拒绝
154 149 /// 在 Android 上:Android 11+ (API 30+):用户是否第二次拒绝权限。低于 Android 11 (API 30):用户是否拒绝访问请求的功能,并选择不再显示请求。
... ... @@ -156,19 +151,14 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
156 151 if (status.isPermanentlyDenied) {
157 152 _isGoSetting = true;
158 153 showAlert(
159   - permission, msgList[2], msgList[3], _isGoSetting ? "前往系统设置" : "确定");
  154 + permission, msgList[2], msgList[3], _isGoSetting ? "前往系统设置" : "继续");
160 155 }
161   - // 拥有部分权限(受限,仅在 iOS (iOS14+) 上受支持)
162   - if (status.isLimited) {
  156 + // isLimited:拥有部分权限(受限,仅在 iOS (iOS14+) 上受支持)
  157 + // isRestricted:拥有部分权限,活动限制(例如,设置了家长///控件,仅在iOS以上受支持。(仅限 iOS)
  158 + if (status.isLimited || status.isRestricted) {
163 159 if (Platform.isIOS || Platform.isMacOS) _isGoSetting = true;
164 160 showAlert(
165   - permission, msgList[1], msgList[3], _isGoSetting ? "前往系统设置" : "确定");
166   - }
167   - // 拥有部分权限,活动限制(例如,设置了家长///控件,仅在iOS以上受支持。(仅限 iOS)
168   - if (status.isRestricted) {
169   - if (Platform.isIOS || Platform.isMacOS) _isGoSetting = true;
170   - showAlert(
171   - permission, msgList[1], msgList[3], _isGoSetting ? "前往系统设置" : "确定");
  161 + permission, msgList[1], msgList[3], _isGoSetting ? "前往系统设置" : "继续");
172 162 }
173 163 }
174 164  
... ... @@ -192,7 +182,7 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
192 182 onPressed: () {
193 183 widget.isRequiredPermission
194 184 ? _quitApp()
195   - : _popDialogAndPage(context, false);
  185 + : _popDialog(context, false);
196 186 }),
197 187 TextButton(
198 188 child: Text(confirmMsg),
... ... @@ -202,13 +192,14 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
202 192 } else {
203 193 _handlePermission(widget.permissions);
204 194 }
205   - _popDialog(context);
  195 + _popDialog(context, null);
206 196 })
207 197 ],
208 198 );
209 199 }).then((value) => {
210 200 _isDialogShowing = false,
211   - });
  201 + _popPage(value)
  202 + });
212 203 }
213 204  
214 205 @override
... ... @@ -217,29 +208,23 @@ class _PermissionRequestPageState extends State&lt;PermissionRequestPage&gt;
217 208 super.dispose();
218 209 }
219 210  
220   - @override
221   - Widget build(BuildContext context) {
222   - return Container();
223   - }
224   -
225 211 /// 退出应用程序
226 212 void _quitApp() {
227 213 AppConfigHelper.exitApp();
228 214 }
229 215  
230   - /// 关闭整个权限申请页面
231   - void _popDialogAndPage(BuildContext dialogContext, bool isAllGranted) {
232   - _popDialog(dialogContext);
233   - _popPage(isAllGranted);
234   - }
235   -
236 216 /// 关闭弹窗
237   - void _popDialog(BuildContext dialogContext) {
  217 + /// isAllGranted为null的话跳到系统设置页
  218 + void _popDialog(BuildContext dialogContext, bool? isAllGranted) {
238 219 Navigator.of(dialogContext).pop();
  220 + _popPage(isAllGranted);
239 221 }
240 222  
241   - /// 关闭透明页面
242   - void _popPage(bool isAllGranted) {
243   - Navigator.of(context).pop(isAllGranted);
  223 + /// 关闭权限申请透明页面
  224 + /// isAllGranted 所有权限都授予,为空不关闭
  225 + void _popPage(bool? isAllGranted) {
  226 + if (isAllGranted != null) {
  227 + Navigator.of(context).pop(isAllGranted);
  228 + }
244 229 }
245 230 }
... ...
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` 以及对应的权限名称 `permissionTypeStr`,如果有权限则返回 `Future true`
  8 +/// permissionDesc:设备权限使用说明(描述)
  9 +/// isRequiredPermission:是否强制要求 如果为 `true`,则 "取消" 按钮将执行 "退出app" 的操作
  10 +Future<bool> requestPermission(
  11 + BuildContext context,
  12 + Permission permission,
  13 + String permissionName,
  14 + String permissionDesc,
  15 + {bool isRequiredPermission = false}) async {
  16 + return requestPermissions(context, [permission], [permissionName], permissionDesc,
  17 + isRequiredPermission: isRequiredPermission);
  18 +}
  19 +
  20 +
  21 +Future<bool> requestPermissions(
  22 + BuildContext context,
  23 + List<Permission> permissions,
  24 + List<String> permissionNames,
  25 + String permissionDesc,
  26 + {bool isRequiredPermission = false}) async {
  27 +
  28 + // List<PermissionStatus> statuses = await Future.wait(
  29 + // permissions.map((permission) => permission.status),
  30 + // );
  31 + // bool allGranted = statuses.every((status) => status.isGranted);
  32 +
  33 + bool allGranted = await isPermissionsGranted(permissions);
  34 + if (allGranted) {
  35 + return true;
  36 + } else {
  37 + return await Navigator.of(context).push(PageRouteBuilder(
  38 + opaque: false,
  39 + pageBuilder: ((context, animation, secondaryAnimation) {
  40 + return PermissionRequestPage(permissions, permissionNames, permissionDesc,
  41 + isRequiredPermission: isRequiredPermission);
  42 + })));
  43 + }
  44 +}
  45 +
  46 +///(实际请求前)判断权限数组是否都授予
  47 +Future<bool> isPermissionsGranted(List<Permission> permissions) async {
  48 + // 使用 every 直接检查权限状态
  49 + return await Future.wait(permissions.map((permission) async {
  50 + return await permission.status.isGranted;
  51 + })).then((statuses) => statuses.every((status) => status));
  52 +}
  53 +
  54 +///请求权限
  55 +Future<MapEntry<Permission, PermissionStatus>?> requestPermissionsInner(List<Permission> permissionList) async {
  56 + Map<Permission, PermissionStatus> statusesMap = await permissionList.request();
  57 + for (var entry in statusesMap.entries) {
  58 + if (!entry.value.isGranted) {
  59 + return entry;
  60 + }
  61 + }
  62 + return null;
  63 +}
0 64 \ 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/user_avatar_bloc/user_avatar_bloc.dart
... ... @@ -14,7 +14,7 @@ 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/permissionRequestPage.dart';
  17 +import '../../../../common/permission/permissionRequester.dart';
18 18  
19 19 part 'user_avatar_event.dart';
20 20 part 'user_avatar_state.dart';
... ... @@ -60,7 +60,8 @@ class UserAvatarBloc extends Bloc&lt;UserAvatarEvent, UserAvatarState&gt; {
60 60 } else {
61 61 permission = Permission.photos;
62 62 }
63   - await getPermissionStatus(permission).then((value) async {
  63 + await requestPermission(context, permission, "文件读取", "用于在更换头像场景下从相册中选取图片等文件").then((value) async {
  64 + // await getPermissionStatus(permission).then((value) async {
64 65 if (!value) {
65 66 debugPrint('失败$value');
66 67 return;
... ... @@ -79,7 +80,7 @@ class UserAvatarBloc extends Bloc&lt;UserAvatarEvent, UserAvatarState&gt; {
79 80 }
80 81  
81 82 void _getImageFromCamera(GetImageFromCameraEvent event, Emitter<UserAvatarState> emitter) async {
82   - bool result = await permissionCheckAndRequest(context, Permission.camera, "拍照");
  83 + bool result = await requestPermission(context, Permission.camera, "拍照", "用于在更换头像场景下调用相机拍照");
83 84 if (result) {
84 85 _file = await picker.pickImage(source: ImageSource.camera);
85 86 EasyLoading.show();
... ...