Skip to content

Commit af13496

Browse files
committed
Refine sensitive permission handling and app permissions
1 parent d8064fa commit af13496

17 files changed

Lines changed: 521 additions & 74 deletions

File tree

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
22
xmlns:tools="http://schemas.android.com/tools">
33

4-
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
5-
64
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
75
</manifest>

android/app/src/main/AndroidManifest.xml

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,16 @@
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
22
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
33
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
4-
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY"/>
5-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
4+
<uses-permission android:name="android.permission.CAMERA" tools:node="remove"/>
65
<uses-permission android:name="android.permission.INTERNET"/>
76
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
8-
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
9-
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
7+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
108
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
11-
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
129
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>
1310
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
14-
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
15-
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
16-
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
11+
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove"/>
1712
<uses-permission android:name="android.permission.VIBRATE"/>
18-
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
13+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" tools:replace="android:maxSdkVersion"/>
1914
<queries>
2015
<intent>
2116
<action android:name="android.intent.action.VIEW"/>

app.json

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@
4141
"bundleIdentifier": "release-20240916",
4242
"buildNumber": "23",
4343
"infoPlist": {
44-
"NSPhotoLibraryUsageDescription": "我们需要访问您的照片库用于保存截图。",
45-
"NSPhotoLibraryAddUsageDescription": "我们需要将图片保存到您的照片库。",
44+
"NSLocationWhenInUseUsageDescription": "用于地图功能获取您的当前位置并提供定位服务。",
45+
"NSPhotoLibraryUsageDescription": "用于选择您主动添加的图片,以完成反馈或设置课表背景。",
46+
"NSPhotoLibraryAddUsageDescription": "用于将您主动生成的课表截图保存到相册。",
4647
"NSAppTransportSecurity": {
4748
"NSAllowsArbitraryLoads": true
4849
},
@@ -63,21 +64,35 @@
6364
"backgroundColor": "#6666FF"
6465
},
6566
"permissions": [
66-
"android.permission.SCHEDULE_EXACT_ALARM",
67-
"android.permission.POST_NOTIFICATIONS",
68-
"android.permission.ACCESS_NOTIFICATION_POLICY",
69-
"android.permission.RECEIVE_BOOT_COMPLETED",
70-
"android.permission.ACCESS_FINE_LOCATION",
7167
"android.permission.ACCESS_COARSE_LOCATION",
72-
"android.permission.FOREGROUND_SERVICE"
68+
"android.permission.ACCESS_FINE_LOCATION",
69+
"android.permission.POST_NOTIFICATIONS",
70+
"android.permission.RECEIVE_BOOT_COMPLETED"
7371
],
7472
"package": "com.muxixyz.ccnubox",
7573
"versionCode": 23
7674
},
7775
"plugins": [
76+
"./plugins/remove-unused-ios-permissions",
7877
"expo-router",
7978
"expo-web-browser",
8079
"expo-font",
80+
[
81+
"expo-image-picker",
82+
{
83+
"photosPermission": "用于选择您主动添加的图片,以完成反馈或设置课表背景。",
84+
"cameraPermission": false,
85+
"microphonePermission": false
86+
}
87+
],
88+
[
89+
"expo-media-library",
90+
{
91+
"photosPermission": "用于访问您主动选择的图片。",
92+
"savePhotosPermission": "用于将您主动生成的课表截图保存到相册。",
93+
"granularPermissions": ["photo"]
94+
}
95+
],
8196
[
8297
"react-native-edge-to-edge",
8398
{

docs/permissions.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# 敏感权限清单与来源
2+
3+
## 统一申请规则
4+
5+
运行时敏感权限必须通过 `src/utils/requestSensitivePermission.ts`
6+
申请。调用方只提供:
7+
8+
- 当前权限查询函数;
9+
- 系统权限申请函数;
10+
- 如何判断已授权;
11+
- 对应的用途说明文案。
12+
13+
该模块负责在系统权限框出现前渲染用途说明、等待用户确认、保持说明直到系统请求结束,并串行化并发申请。每种用途在用户点击“继续”后持久化确认状态,后续不再重复展示;点击“取消”不会记录确认状态。
14+
15+
当前业务入口:
16+
17+
- 反馈上传图片:通过系统 Photo Picker 选择图片,不申请媒体库读取权限;
18+
- 课表背景图片:通过系统 Photo Picker 选择图片,不申请媒体库读取权限;
19+
- 保存课表截图:相册写入权限;
20+
- 开启消息推送:通知权限。
21+
- 校园地图:使用期间定位权限。
22+
23+
## 冗余权限的来源
24+
25+
`android.permissions` 只描述应用显式声明的权限,但 Expo config
26+
plugin 会在解析配置时继续合并权限:
27+
28+
- `expo-media-library` 总是添加
29+
`READ_EXTERNAL_STORAGE``WRITE_EXTERNAL_STORAGE`
30+
`READ_MEDIA_VISUAL_USER_SELECTED`;未配置 `granularPermissions`
31+
时还会默认添加图片、视频、音频读取权限。
32+
- `expo-image-picker``microphonePermission` 未设为 `false` 时添加
33+
`RECORD_AUDIO`,并可能写入相机相关声明。
34+
- `android/app/src/main/AndroidManifest.xml``ios/ccnubox/Info.plist`
35+
是 prebuild 产物,不应作为长期权限配置的唯一来源;手动删除产物中的声明会在下一次 prebuild 时被恢复。
36+
37+
因此配置采取以下策略:
38+
39+
1.`app.json` 为图片插件指定最小用途文案,并只请求
40+
`granularPermissions: ["photo"]`;图片读取和旧版 Android 相册写入所需的基础权限由插件保留。
41+
2.`expo-image-picker``cameraPermission``microphonePermission` 设为
42+
`false`,由官方插件阻止相机和录音权限进入最终 Manifest。
43+
3.`android.permissions`
44+
中移除没有业务用途的显式权限;该数组只用于添加应用确实需要且依赖没有自动声明的权限。
45+
4. 运行 `remove-unused-ios-permissions`,移除 `expo-secure-store`
46+
自动加入但应用没有使用的 Face ID 权限说明。
47+
48+
Android 的 `expo-image-picker` 使用系统 Photo
49+
Picker。选取反馈图片和课表背景时,先展示必须确认的用途说明,再直接打开 Photo
50+
Picker;不调用
51+
`requestMediaLibraryPermissionsAsync`。系统只向应用提供用户主动选择的图片。
52+
53+
如果后续依赖的原生 Manifest 自动加入了无法通过其配置关闭的权限,应优先在
54+
`android.blockedPermissions` 中声明。Expo 会在最终 Manifest 中生成
55+
`tools:node="remove"`;不需要为此维护自定义 Android 权限过滤插件。
56+
57+
React Native 的 debug 依赖会声明
58+
`SYSTEM_ALERT_WINDOW`,但 release 依赖不会。权限审计应以 release 合并后的 Manifest 为准。
59+
60+
修改权限配置后,应重新运行 prebuild 并检查生成的 Android Manifest 和 iOS
61+
Info.plist;不要只修改生成文件。

ios/ccnubox/Info.plist

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,14 @@
6868
<array>
6969
<string>_expo._tcp</string>
7070
</array>
71-
<key>NSCameraUsageDescription</key>
72-
<string>Allow $(PRODUCT_NAME) to access your camera</string>
73-
<key>NSFaceIDUsageDescription</key>
74-
<string>Allow $(PRODUCT_NAME) to access your Face ID biometric data.</string>
7571
<key>NSLocalNetworkUsageDescription</key>
7672
<string>Expo Dev Launcher uses the local network to discover and connect to development servers running on your computer.</string>
77-
<key>NSMicrophoneUsageDescription</key>
78-
<string>Allow $(PRODUCT_NAME) to access your microphone</string>
73+
<key>NSLocationWhenInUseUsageDescription</key>
74+
<string>用于地图功能获取您的当前位置并提供定位服务。</string>
7975
<key>NSPhotoLibraryAddUsageDescription</key>
80-
<string>我们需要将图片保存到您的照片库</string>
76+
<string>用于将您主动生成的课表截图保存到相册</string>
8177
<key>NSPhotoLibraryUsageDescription</key>
82-
<string>我们需要访问您的照片库用于保存截图</string>
78+
<string>用于选择您主动添加的图片,以完成反馈或设置课表背景</string>
8379
<key>NSSupportsLiveActivities</key>
8480
<true/>
8581
<key>NSUserActivityTypes</key>
@@ -120,4 +116,4 @@
120116
<key>UIViewControllerBasedStatusBarAppearance</key>
121117
<false/>
122118
</dict>
123-
</plist>
119+
</plist>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
const { withInfoPlist } = require('expo/config-plugins');
2+
3+
/**
4+
* Keep unused iOS permission descriptions out of generated Info.plist files.
5+
* This app does not use the Face ID APIs exposed by expo-secure-store.
6+
*/
7+
module.exports = function removeUnusedIosPermissions(config) {
8+
return withInfoPlist(config, config => {
9+
delete config.modResults.NSFaceIDUsageDescription;
10+
return config;
11+
});
12+
};

src/app/(mainPage)/map.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { WebView } from 'react-native-webview';
44
export default function Map() {
55
return (
66
<WebView
7+
geolocationEnabled
78
style={styles.container}
89
source={{ uri: 'https://gis.ccnu.edu.cn/' }}
910
/>

src/app/(mainPage)/webview.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,10 @@ export default function Webview() {
7777
ref={webview}
7878
javaScriptEnabled
7979
// injectedJavaScript={login}
80-
geolocationEnabled
8180
injectedJavaScriptForMainFrameOnly={false}
8281
originWhitelist={['*']}
8382
setSupportMultipleWindows={false} // Android 必须
84-
onShouldStartLoadWithRequest={request => true}
83+
onShouldStartLoadWithRequest={_request => true}
8584
onNavigationStateChange={autoLogin}
8685
style={styles.container}
8786
onMessage={event => {

src/app/(setting)/feedback/writefeedback.tsx

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ import {
2626
ISSUE_TYPE_MAP,
2727
MODULE_MAP,
2828
} from '@/constants/FEEDBACKS';
29+
import { SENSITIVE_PERMISSION_PURPOSES } from '@/constants/SENSITIVE_PERMISSIONS';
2930
import { createFeedbackRecord } from '@/request/api/feedback';
30-
import { log } from '@/utils/logger';
31+
import { runSensitiveAction } from '@/utils/requestSensitivePermission';
3132
import { uploadFileToFeishuBitable } from '@/utils/uploadPicture';
3233

3334
type ImageItem = {
@@ -89,24 +90,18 @@ function WriteFeedback() {
8990
};
9091

9192
const handleSelectImage = async () => {
92-
Toast.info('应用将申请相册权限用于上传图片');
9393
try {
94-
const { status } =
95-
await ImagePicker.requestMediaLibraryPermissionsAsync();
96-
if (status !== 'granted') {
97-
log.error('相册权限被拒绝');
98-
Toast.fail('权限被拒绝,需要相册权限来选择图片');
99-
return;
100-
}
101-
102-
const result = await ImagePicker.launchImageLibraryAsync({
103-
mediaTypes: 'images',
104-
allowsEditing: true,
105-
quality: 0.7,
106-
aspect: [4, 3],
94+
const result = await runSensitiveAction({
95+
action: () =>
96+
ImagePicker.launchImageLibraryAsync({
97+
mediaTypes: 'images',
98+
allowsEditing: true,
99+
quality: 0.7,
100+
aspect: [4, 3],
101+
}),
102+
purpose: SENSITIVE_PERMISSION_PURPOSES.feedbackImage,
107103
});
108-
109-
if (result.canceled) return;
104+
if (!result || result.canceled) return;
110105

111106
const assets = result.assets || [];
112107
if (assets.length === 0) return;

src/app/_layout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
88
import { SafeAreaProvider } from 'react-native-safe-area-context';
99
import WebView from 'react-native-webview';
1010

11+
import SensitivePermissionNotice from '@/components/sensitivePermissionNotice';
12+
1113
import PortalRoot from '../components/portal';
1214
import Scraper from '../components/scraper';
1315
import useBadgeSync from '../hooks/useBadgeSync';
@@ -110,6 +112,7 @@ export default function RootLayout() {
110112
</Stack>
111113
{/* portal */}
112114
<PortalRoot ref={portalRef} />
115+
<SensitivePermissionNotice />
113116
</SafeAreaProvider>
114117
</GestureHandlerRootView>
115118
</Provider>

0 commit comments

Comments
 (0)