A
第 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_CALENDAR
  • CAMERA:CAMERA
  • CONTACTS:READ_CONTACTS、WRITE_CONTACTS、GET_ACCOUNTS
  • LOCATION:ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION
  • MICROPHONE:RECORD_AUDIO
  • PHONE:READ_PHONE_STATE、CALL_PHONE、READ_SMS
  • SENSORS:BODY_SENSORS
  • STORAGE:READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE(旧版),Android 13+ 拆为 READ_MEDIA_IMAGES / VIDEO / AUDIO
  • NOTIFICATIONS: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);
    }
}

常见坑与最佳实践

  1. 在 onCreate 立刻弹权限:用户还没看到界面就被系统弹窗打断,体验差。最佳实践是先展示一个“为什么需要”的预弹窗,等用户点击触发再申请。
  2. shouldShowRequestPermissionRationale 误用:第一次申请前为 false;勾选“不再询问”后也变成 false。要区分这两种情况,需要记录是否已申请过(可用 SharedPreferences)。
  3. 没有声明 <uses-permission> 就直接 requestPermissions:直接返回被拒绝,调试时容易踩坑。
  4. 未声明 targetSdkVersion 适配:低 targetSdk 下部分危险权限被自动授予,发布到 Google Play 必须满足其 targetSdk 要求。
  5. Android 13+ 仍申请 READ_EXTERNAL_STORAGE:在 Android 13+ 上不生效,需要改为 READ_MEDIA_IMAGES/VIDEO/AUDIO 或用 Photo Picker。
  6. CALL_PHONE 与 manifest-only 权限:某些权限(如 READ_PHONE_STATE)在 Android 11+ 进一步细粒度化,需要按设备版本动态选择申请项。
  7. Android 12 蓝牙权限:BLUETOOTH_SCAN、BLUETOOTH_CONNECT 必须运行时申请,否则扫描/连接失败。
  8. PermissionUtils.openAppSettings 后无法返回结果:用户回到 App 后可在 onResume 重新检查权限状态并继续流程。

章节小结

  • 权限按保护级别分为 normal / dangerous / signature,dangerous 必须运行时申请;
  • Manifest 用 <uses-permission> 声明,Android 13+ 媒体权限细粒度化;
  • 现代 ActivityResultContracts.RequestPermission / RequestMultiplePermissions 替代旧 API;
  • 用户拒绝时通过 shouldShowRequestPermissionRationale 区分“还能再弹”与“已被禁止”,后者引导到系统设置。

下一章预告

第 15 章将讲解 通知 Notification:通知渠道(Channel)的创建、基本通知构建、大图/大文本/收件箱样式、点击 PendingIntent 跳转、进度通知、前台服务通知以及 Android 13+ 通知权限申请。