第 14 章JAVA35 分钟
权限管理
掌握 Android 权限分类、清单声明、运行时申请、批量申请与拒绝后引导设置,构建合规且用户友好的权限流程
学习目标
- 区分 normal、dangerous、signature 三类权限的申请方式
- 学会用清单声明权限并配合运行时申请
- 使用 ActivityResultContracts.RequestPermission 简化单个/多个权限申请
- 在用户拒绝权限时给出说明并引导到系统设置页
学习目标
Android 把权限分为多类,普通权限安装时即可授予,危险权限必须在运行时让用户主动确认。本章目标:
- 理解 normal / dangerous / signature 权限的差异;
- 在 Manifest 正确声明权限并配合
uses-permission; - 用现代
ActivityResultContracts申请单个或多个权限; - 在用户拒绝(含“不再询问”)时给出说明并引导到系统设置。
Android 权限分类
1. 三种保护级别
| 级别 | 说明 | 申请方式 |
|---|---|---|
normal |
风险较低,如 INTERNET、VIBRATE |
仅需在 Manifest 声明,安装即授 |
dangerous |
涉及隐私或敏感数据,如 READ_CONTACTS、ACCESS_FINE_LOCATION |
Manifest 声明 + 运行时申请 |
signature |
仅相同签名的 App 才能获得 | 仅声明,系统按签名授权 |
2. 常见 dangerous 权限组
CALENDAR:READ_CALENDAR、WRITE_CALENDARCAMERA:CAMERACONTACTS:READ_CONTACTS、WRITE_CONTACTS、GET_ACCOUNTSLOCATION:ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATIONMICROPHONE:RECORD_AUDIOPHONE:READ_PHONE_STATE、CALL_PHONE、READ_SMSSENSORS:BODY_SENSORSSTORAGE:READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE(旧版),Android 13+ 拆为READ_MEDIA_IMAGES / VIDEO / AUDIONOTIFICATIONS:POST_NOTIFICATIONS(Android 13+)
清单声明权限
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 普通权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- 危险权限 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Android 13+ 媒体细粒度 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<!-- Android 12+ 蓝牙细粒度 -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<application ...>
</application>
</manifest>
兼容旧版的 maxSdkVersion
<!-- 在 Android 10 及以下需要写存储权限 -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
运行时申请权限
1. 旧 API:requestPermissions
public class CameraActivity extends AppCompatActivity {
private static final int REQ_CAMERA = 1001;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
findViewById(R.id.btn_open_camera).setOnClickListener(v -> {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
openCamera();
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(
this, Manifest.permission.CAMERA)) {
showWhyCameraDialog();
} else {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA}, REQ_CAMERA);
}
}
});
}
private void showWhyCameraDialog() {
new AlertDialog.Builder(this)
.setTitle("需要相机权限")
.setMessage("为了能拍摄照片,请允许使用相机。")
.setPositiveButton("去授权", (d, w) ->
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA}, REQ_CAMERA))
.setNegativeButton("取消", null)
.show();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQ_CAMERA) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openCamera();
} else {
Toast.makeText(this, "相机权限被拒绝", Toast.LENGTH_SHORT).show();
}
}
}
private void openCamera() {
// ... 打开相机
}
}
2. 现代 API:ActivityResultContracts.RequestPermission
public class CameraActivity extends AppCompatActivity {
private final ActivityResultLauncher<String> mRequestCamera =
registerForActivityResult(new ActivityResultContracts.RequestPermission(),
granted -> {
if (granted) {
openCamera();
} else {
if (!ActivityCompat.shouldShowRequestPermissionRationale(
this, Manifest.permission.CAMERA)) {
// 用户勾选了"不再询问"
showGoToSettingsDialog();
} else {
Toast.makeText(this, "相机权限被拒绝",
Toast.LENGTH_SHORT).show();
}
}
});
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
findViewById(R.id.btn_open_camera).setOnClickListener(v -> {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED) {
openCamera();
} else {
mRequestCamera.launch(Manifest.permission.CAMERA);
}
});
}
private void showGoToSettingsDialog() {
new AlertDialog.Builder(this)
.setTitle("需要相机权限")
.setMessage("您已在系统弹窗中勾选了"不再询问"。\n请到设置中手动开启相机权限。")
.setPositiveButton("去设置", (d, w) -> openAppSettings())
.setNegativeButton("取消", null)
.show();
}
private void openAppSettings() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
}
同时申请多个权限
1. 使用 RequestMultiplePermissions
private final ActivityResultLauncher<String[]> mRequestMultiple =
registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
result -> {
boolean camera = Boolean.TRUE.equals(result.get(Manifest.permission.CAMERA));
boolean location = Boolean.TRUE.equals(
result.get(Manifest.permission.ACCESS_FINE_LOCATION));
if (camera && location) {
startScan();
} else {
Toast.makeText(this, "部分权限被拒绝,无法启动",
Toast.LENGTH_SHORT).show();
}
});
private void tryRequestAll() {
List<String> needed = new ArrayList<>();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.CAMERA);
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.ACCESS_FINE_LOCATION);
}
if (needed.isEmpty()) {
startScan();
} else {
mRequestMultiple.launch(needed.toArray(new String[0]));
}
}
2. 引导用户到系统设置
private void openAppSettings() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
3. 用 EasyPermissions 库(可选)
implementation 'pub.devrel:easypermissions:3.0.0'
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
@AfterPermissionGranted(REQ_CAMERA)
private void onCameraPermissionGranted() {
openCamera();
}
Android 13+ 通知权限
Android 13 起需要 POST_NOTIFICATIONS:
private final ActivityResultLauncher<String> mRequestNotification =
registerForActivityResult(new ActivityResultContracts.RequestPermission(),
granted -> {
if (granted) {
// 启用通知
} else {
// 用户拒绝通知
}
});
private void ensureNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
mRequestNotification.launch(Manifest.permission.POST_NOTIFICATIONS);
}
}
}
封装权限工具类
public final class PermissionUtils {
private PermissionUtils() { }
public static boolean hasPermission(Context context, String permission) {
return ContextCompat.checkSelfPermission(context, permission)
== PackageManager.PERMISSION_GRANTED;
}
public static boolean hasAllPermissions(Context context, String[] permissions) {
for (String p : permissions) {
if (!hasPermission(context, p)) return false;
}
return true;
}
public static boolean shouldShowRationale(Activity activity, String permission) {
return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission);
}
public static void openAppSettings(Context context) {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + context.getPackageName()));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
常见坑与最佳实践
- 在
onCreate立刻弹权限:用户还没看到界面就被系统弹窗打断,体验差。最佳实践是先展示一个“为什么需要”的预弹窗,等用户点击触发再申请。 shouldShowRequestPermissionRationale误用:第一次申请前为false;勾选“不再询问”后也变成false。要区分这两种情况,需要记录是否已申请过(可用SharedPreferences)。- 没有声明
<uses-permission>就直接requestPermissions:直接返回被拒绝,调试时容易踩坑。 - 未声明
targetSdkVersion适配:低 targetSdk 下部分危险权限被自动授予,发布到 Google Play 必须满足其 targetSdk 要求。 - Android 13+ 仍申请
READ_EXTERNAL_STORAGE:在 Android 13+ 上不生效,需要改为READ_MEDIA_IMAGES/VIDEO/AUDIO或用 Photo Picker。 CALL_PHONE与 manifest-only 权限:某些权限(如READ_PHONE_STATE)在 Android 11+ 进一步细粒度化,需要按设备版本动态选择申请项。- Android 12 蓝牙权限:
BLUETOOTH_SCAN、BLUETOOTH_CONNECT必须运行时申请,否则扫描/连接失败。 PermissionUtils.openAppSettings后无法返回结果:用户回到 App 后可在onResume重新检查权限状态并继续流程。
章节小结
- 权限按保护级别分为 normal / dangerous / signature,dangerous 必须运行时申请;
- Manifest 用
<uses-permission>声明,Android 13+ 媒体权限细粒度化; - 现代
ActivityResultContracts.RequestPermission / RequestMultiplePermissions替代旧 API; - 用户拒绝时通过
shouldShowRequestPermissionRationale区分“还能再弹”与“已被禁止”,后者引导到系统设置。
下一章预告
第 15 章将讲解 通知 Notification:通知渠道(Channel)的创建、基本通知构建、大图/大文本/收件箱样式、点击 PendingIntent 跳转、进度通知、前台服务通知以及 Android 13+ 通知权限申请。