《Android 应用开发基础教程》——第十三章:权限管理机制与运行时权限请求(以拍照/存储为例)

目录

第十三章:权限管理机制与运行时权限请求(以拍照/存储为例)

[🔸 13.1 权限分类](#🔸 13.1 权限分类)

[🔸 13.2 权限声明方式](#🔸 13.2 权限声明方式)

[🔸 13.3 动态请求权限完整流程](#🔸 13.3 动态请求权限完整流程)

[✦ 1. 检查是否已授权:](#✦ 1. 检查是否已授权:)

[✦ 2. 请求权限:](#✦ 2. 请求权限:)

[✦ 3. 重写回调处理结果:](#✦ 3. 重写回调处理结果:)

[🔸 13.4 示例一:拍照权限请求 + 拍照功能](#🔸 13.4 示例一:拍照权限请求 + 拍照功能)

[✦ 权限需求:](#✦ 权限需求:)

[✦ 实现代码:](#✦ 实现代码:)

[🔸 13.5 示例二:访问存储权限请求 + 文件读取](#🔸 13.5 示例二:访问存储权限请求 + 文件读取)

[✦ 权限需求(Android 10 以下):](#✦ 权限需求(Android 10 以下):)

[✦ 动态请求 + 文件选择器:](#✦ 动态请求 + 文件选择器:)

[🔸 13.6 针对不同版本的兼容处理](#🔸 13.6 针对不同版本的兼容处理)

[🔸 13.7 权限请求的 UX 建议](#🔸 13.7 权限请求的 UX 建议)

[✅ 总结](#✅ 总结)

习题答案

项目结构

[1. AndroidManifest.xml](#1. AndroidManifest.xml)

[2. MainActivity.java](#2. MainActivity.java)

[3. PermissionsUtils.java](#3. PermissionsUtils.java)

[4. activity_main.xml](#4. activity_main.xml)

[5. 分区存储(Scoped Storage)适配](#5. 分区存储(Scoped Storage)适配)

拍照示例

存储读取示例

总结


第十三章:权限管理机制与运行时权限请求(以拍照/存储为例)


Android 系统从 6.0(API 23)开始引入了运行时权限机制。某些危险权限(如拍照、读取外部存储、定位等)必须在应用运行时动态请求。本章将系统讲解 Android 权限分类、请求流程,并通过拍照与文件存储两个实例来演示运行时权限的完整用法。


🔸 13.1 权限分类

Android 权限主要分为两类:

权限类型 示例权限 是否需要运行时请求
正常权限(Normal) INTERNET、VIBRATE 否,只需在 manifest 声明
危险权限(Dangerous) CAMERA、READ_EXTERNAL_STORAGE 是,需 manifest 声明 + 动态请求

🔸 13.2 权限声明方式

AndroidManifest.xml 中声明需要的权限:

XML 复制代码
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

注意:从 Android 10 开始推荐使用 MediaStore API 和 scoped storage,WRITE_EXTERNAL_STORAGE 权限有被限制的趋势。


🔸 13.3 动态请求权限完整流程

✦ 1. 检查是否已授权:

java 复制代码
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        != PackageManager.PERMISSION_GRANTED) {
    // 需要请求权限
}

✦ 2. 请求权限:

java 复制代码
ActivityCompat.requestPermissions(this,
        new String[]{Manifest.permission.CAMERA},
        1001);

✦ 3. 重写回调处理结果:

java 复制代码
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    if (requestCode == 1001) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 权限已授予,执行拍照等操作
        } else {
            Toast.makeText(this, "权限被拒绝", Toast.LENGTH_SHORT).show();
        }
    }
}

🔸 13.4 示例一:拍照权限请求 + 拍照功能

✦ 权限需求:

XML 复制代码
<uses-permission android:name="android.permission.CAMERA" />

✦ 实现代码:

java 复制代码
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this,
            new String[]{Manifest.permission.CAMERA}, 1001);
} else {
    openCamera();
}

private void openCamera() {
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    startActivityForResult(intent, 2001);
}

注意:Android 11 后强烈推荐使用 FileProvider 与 scoped storage 保存拍照文件。


🔸 13.5 示例二:访问存储权限请求 + 文件读取

✦ 权限需求(Android 10 以下):

XML 复制代码
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

✦ 动态请求 + 文件选择器:

java 复制代码
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this,
            new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1002);
} else {
    openFileChooser();
}

private void openFileChooser() {
    Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
    intent.setType("*/*");
    startActivityForResult(intent, 2002);
}

🔸 13.6 针对不同版本的兼容处理

java 复制代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    // Android 13+: 特定权限如 READ_MEDIA_IMAGES
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    // Android 6.0~12:需运行时申请 CAMERA/READ_EXTERNAL_STORAGE 等
}

🔸 13.7 权限请求的 UX 建议

  1. 解释权限用途(使用 AlertDialog 引导用户)

  2. 多次拒绝后,可提示用户去系统设置页面手动开启

  3. 权限被永久拒绝的处理:

java 复制代码
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
    // 跳转设置页
    Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    intent.setData(Uri.parse("package:" + getPackageName()));
    startActivity(intent);
}

✅ 总结

  • 危险权限必须同时在 manifest 和代码中动态申请

  • 权限检查流程必须覆盖所有可能状态(已授权、拒绝、永久拒绝)

  • 拍照、存储、定位是最常见的权限请求场景

  • 从 Android 10 起应考虑适配"分区存储(scoped storage)"


📢 下一章预告:

第十四章:Android 多线程编程与异步任务机制(Handler、AsyncTask、线程池等)


习题答案

如何在 Android 中处理 危险权限 的动态申请,并覆盖所有可能的状态(已授权、拒绝、永久拒绝)。同时,针对 拍照、存储、定位 这些常见的权限请求场景,提供适配代码,并考虑从 Android 10 起的 分区存储(Scoped Storage)


项目结构

javascript 复制代码
MainActivity.java
PermissionsUtils.java
activity_main.xml
AndroidManifest.xml

1. AndroidManifest.xml

AndroidManifest.xml 中声明所需权限:

XML 复制代码
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.demo">

    <!-- 危险权限 -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" /> <!-- 仅适用于 Android 9 及以下 -->

    <application
        android:allowBackup="true"
        android:label="Demo App"
        android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

2. MainActivity.java

java 复制代码
package com.example.demo;

import android.Manifest;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

public class MainActivity extends AppCompatActivity {

    private static final int PERMISSION_REQUEST_CODE = 100;
    private String[] permissions = {
            Manifest.permission.CAMERA,
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.READ_EXTERNAL_STORAGE
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 检查并申请权限
        checkAndRequestPermissions();
    }

    private void checkAndRequestPermissions() {
        if (PermissionsUtils.hasPermissions(this, permissions)) {
            // 权限已全部授予
            onPermissionsGranted();
        } else {
            // 请求权限
            ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE);
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == PERMISSION_REQUEST_CODE) {
            if (PermissionsUtils.hasPermissions(this, permissions)) {
                // 权限已全部授予
                onPermissionsGranted();
            } else {
                // 检查是否有权限被永久拒绝
                boolean shouldShowRationale = false;
                for (String permission : permissions) {
                    if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
                        shouldShowRationale = true;
                        break;
                    }
                }

                if (shouldShowRationale) {
                    // 用户永久拒绝了权限
                    showPermissionDeniedDialog();
                } else {
                    // 用户拒绝了权限但未选中"不再询问"
                    showPermissionRationaleDialog();
                }
            }
        }
    }

    private void onPermissionsGranted() {
        // 权限已授予权限后的逻辑
        // 例如:启动相机、获取位置等
    }

    private void showPermissionRationaleDialog() {
        new AlertDialog.Builder(this)
                .setTitle("权限说明")
                .setMessage("应用需要这些权限才能正常运行。请授予相关权限。")
                .setPositiveButton("确定", (dialog, which) -> checkAndRequestPermissions())
                .setNegativeButton("取消", null)
                .show();
    }

    private void showPermissionDeniedDialog() {
        new AlertDialog.Builder(this)
                .setTitle("权限被拒绝")
                .setMessage("您已永久拒绝某些权限。请前往设置手动开启权限。")
                .setPositiveButton("去设置", (dialog, which) -> {
                    Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                    intent.setData(Uri.fromParts("package", getPackageName(), null));
                    startActivity(intent);
                })
                .setNegativeButton("取消", null)
                .show();
    }
}

3. PermissionsUtils.java

工具类用于检查权限是否已授予:

java 复制代码
package com.example.demo;

import android.content.Context;
import androidx.core.content.ContextCompat;

public class PermissionsUtils {

    public static boolean hasPermissions(Context context, String... permissions) {
        for (String permission : permissions) {
            if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }
}

4. activity_main.xml

简单的布局文件:

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="16dp">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="点击按钮请求权限"
        android:textSize="18sp"
        android:gravity="center" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="请求权限"
        android:onClick="checkAndRequestPermissions" />
</LinearLayout>

5. 分区存储(Scoped Storage)适配

从 Android 10 开始,推荐使用 分区存储 来访问共享存储中的文件。以下是适配建议:

拍照示例
java 复制代码
private void takePicture() {
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (intent.resolveActivity(getPackageManager()) != null) {
        // 创建一个临时文件保存照片
        File photoFile = createImageFile();
        if (photoFile != null) {
            Uri photoURI = FileProvider.getUriForFile(this, "com.example.demo.fileprovider", photoFile);
            intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
            startActivityForResult(intent, REQUEST_TAKE_PHOTO);
        }
    }
}

private File createImageFile() {
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
    String imageFileName = "JPEG_" + timeStamp + "_";
    File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
    try {
        return File.createTempFile(imageFileName, ".jpg", storageDir);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}
存储读取示例
java 复制代码
private void readFromStorage() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        ContentResolver resolver = getContentResolver();
        Uri collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY);
        String[] projection = {MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME};
        Cursor cursor = resolver.query(collection, projection, null, null, null);
        if (cursor != null) {
            while (cursor.moveToNext()) {
                long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID));
                String displayName = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME));
                // 处理图片
            }
            cursor.close();
        }
    } else {
        // 旧版本逻辑
    }
}

总结

  1. 权限申请 :必须在 manifest 和代码中动态申请。
  2. 权限状态覆盖:处理已授权、拒绝、永久拒绝三种状态。
  3. 常见权限场景:适配拍照、存储、定位。
  4. 分区存储 :从 Android 10 起,使用 MediaStoreContentResolver 访问共享存储。
相关推荐
皮皮林55111 小时前
使用 Java + WebSocket 实现简单实时双人协同 pk 答题
java·websocket
一只柠檬新12 小时前
Web和Android的渐变角度区别
android
志旭12 小时前
从0到 1实现BufferQueue GraphicBuffer fence HWC surfaceflinger
android
_一条咸鱼_12 小时前
Android Runtime堆内存架构设计(47)
android·面试·android jetpack
码小凡12 小时前
优雅!用了这两款插件,我成了整个公司代码写得最规范的码农
java·后端
用户20187928316712 小时前
WMS(WindowManagerService的诞生
android
用户20187928316713 小时前
通俗易懂的讲解:Android窗口属性全解析
android
openinstall13 小时前
A/B测试如何借力openinstall实现用户价值深挖?
android·ios·html
二流小码农13 小时前
鸿蒙开发:资讯项目实战之项目初始化搭建
android·ios·harmonyos
志旭14 小时前
android15 vsync源码分析
android