【Android】权限全面解析:分类、流程、实践及特殊场景处理

512 阅读5分钟

在 Android 开发里,权限是保障用户隐私和系统安全的关键机制。应用若要访问敏感资源或者执行特定操作,就必须先获得相应权限。

权限分类

1. 普通权限

这类权限不会对用户隐私和系统安全造成太大风险,应用在安装时系统会自动授予。

  • 示例:网络访问权限(INTERNET)、设置时区权限(SET_TIME_ZONE)。
  • 特点:应用安装完成后就拥有这些权限,无需用户手动授权。

2. 危险权限

涉及用户敏感数据或者会影响其他应用运行的权限属于危险权限。应用需要在运行时动态向用户申请,且必须得到用户明确许可。

  • 示例:相机权限(CAMERA)、读写存储权限(READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE)、位置权限(ACCESS_FINE_LOCATION)。

  • 特点:

    • 危险权限按权限组划分,同一组内的权限,用户授予一个后,应用就可使用该组的其他权限。
    • 必须在运行时进行权限检查和请求。

3. 特殊权限

像 SYSTEM_ALERT_WINDOW(悬浮窗)和 WRITE_SETTINGS(修改系统设置)这类权限属于特殊权限,它们有专门的授权流程。

  • 特点:需要通过特定的 Intent 启动系统设置界面让用户授权。

4. 签名权限

只有当应用与声明该权限的应用使用相同签名时,才能获得的权限。一般用于系统组件或者同一家公司的应用之间交互。

运行时权限处理流程

1. 检查权限

// 检查是否拥有读取联系人的权限
if (ContextCompat.checkSelfPermission(
        this,
        Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {

         // 未获得权限,需要申请

    } else {
        // 已获得权限,可以执行相关操作
        
} 

2. 请求权限

ActivityCompat.requestPermissions(
        this,
        new String[]{Manifest.permission.READ_CONTACTS},
        REQUEST_CODE_PERMISSION_READ_CONTACTS);

3. 处理权限返回结果

通过重写 onRequestPermissionsResult() 方法来处理用户的授权结果。


@Override
public void onRequestPermissionsResult(
        int requestCode,
        String[] permissions,
        int[] grantResults) {

    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_CODE_PERMISSION_READ_CONTACTS) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 用户授予了权限,执行相关操作
            readContacts();
        } else {
            // 用户拒绝了权限
            Toast.makeText(
                    this,
                    "权限被拒绝,无法读取联系人",
                    Toast.LENGTH_SHORT).show();
        }
    }
}

4. 解释请求权限的原因(可选)

// 如果应用之前请求过此权限但用户拒绝了,向用户显示解释
if (ActivityCompat.shouldShowRequestPermissionRationale(
        this,
        Manifest.permission.READ_CONTACTS)) {

    Toast.makeText(
            this,
            "读取联系人权限有助于获取联系人信息",
            Toast.LENGTH_SHORT).show();

    // 再次请求权限
    ActivityCompat.requestPermissions(
            this,
            new String[]{Manifest.permission.READ_CONTACTS},
            REQUEST_CODE_PERMISSION_READ_CONTACTS);

} else {
    // 请求权限
    ActivityCompat.requestPermissions(
            this,
            new String[]{Manifest.permission.READ_CONTACTS},
            REQUEST_CODE_PERMISSION_READ_CONTACTS);
}

5. 完整代码

public class CheckPermissionActivity extends AppCompatActivity {

    private static final int REQUEST_CODE_PERMISSION_READ_CONTACTS = 1001;

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

        Button btnCheckPermission = findViewById(R.id.btnCheckPermission);
        btnCheckPermission.setOnClickListener(v -> checkPermission());
    }

    private void checkPermission() {
        // 检查是否拥有读取联系人的权限
        if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {

            // 如果应用之前请求过此权限但用户拒绝了,向用户显示解释
            if (ActivityCompat.shouldShowRequestPermissionRationale(
                    this,
                    Manifest.permission.READ_CONTACTS)) {

                Toast.makeText(
                        this,
                        "读取联系人权限有助于获取联系人信息",
                        Toast.LENGTH_SHORT).show();

                // 再次请求权限
                ActivityCompat.requestPermissions(
                        this,
                        new String[]{Manifest.permission.READ_CONTACTS},
                        REQUEST_CODE_PERMISSION_READ_CONTACTS);

            } else {
                // 请求权限
                ActivityCompat.requestPermissions(
                        this,
                        new String[]{Manifest.permission.READ_CONTACTS},
                        REQUEST_CODE_PERMISSION_READ_CONTACTS);
            }
        } else {
            // 已经拥有权限,执行相关操作
            readContacts();
        }
    }

    @Override
    public void onRequestPermissionsResult(
            int requestCode,
            String[] permissions,
            int[] grantResults) {

        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE_PERMISSION_READ_CONTACTS) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // 用户授予了权限,执行相关操作
                readContacts();
            } else {
                // 用户拒绝了权限
                Toast.makeText(
                        this,
                        "权限被拒绝,无法读取联系人",
                        Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void readContacts() {
        // 这里添加读取联系人的逻辑
        Toast.makeText(this, "正在读取联系人...", Toast.LENGTH_SHORT).show();
    }
}
  1. 权限检查:使用 ContextCompat.checkSelfPermission() 检查应用是否已经拥有指定的权限。
  2. 请求权限:如果应用没有权限,使用 ActivityCompat.requestPermissions() 请求权限。
  3. 处理用户响应:在 onRequestPermissionsResult() 方法中处理用户对权限请求的响应。
  4. 解释请求原因:如果用户之前拒绝过权限,可以通过 ActivityCompat.shouldShowRequestPermissionRationale() 来决定是否向用户显示解释。

危险权限组

Android 将危险权限划分为多个权限组,同一组内的权限,用户授予一个后,应用就可使用该组的其他权限。以下是一些常见的危险权限组:

权限组包含的权限
CALENDARREAD_CALENDAR、WRITE_CALENDAR
CAMERACAMERA
CONTACTSREAD_CONTACTS、WRITE_CONTACTS、GET_ACCOUNTS
LOCATIONACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION
MICROPHONERECORD_AUDIO
PHONEREAD_PHONE_STATE、CALL_PHONE、READ_CALL_LOG、WRITE_CALL_LOG 等
SENSORSBODY_SENSORS
SMSSEND_SMS、RECEIVE_SMS、READ_SMS 等
STORAGEREAD_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE

特殊权限处理

悬浮窗权限(SYSTEM_ALERT_WINDOW)

1. AndroidManifest.xml

在应用的 AndroidManifest.xml 文件中添加悬浮窗权限声明:

<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />

2. MainActivity.java

public class floatingViewPermissionActivity extends AppCompatActivity {

    private static final int REQUEST_CODE_OVERLAY_PERMISSION = 1;
    private View floatingView;
    private WindowManager windowManager;
    private WindowManager.LayoutParams params;

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

        Button btnShowFloatingWindow = findViewById(R.id.btnShowFloatingWindow);
        btnShowFloatingWindow.setOnClickListener(v -> checkOverlayPermission());
    }

    private void checkOverlayPermission() {
        // Check if the permission is already granted
        if (Settings.canDrawOverlays(this)) {
            showFloatingWindow();
        } else {
            // Request the permission if not granted
            Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
                    Uri.parse("package:" + getPackageName()));
            startActivityForResult(intent, REQUEST_CODE_OVERLAY_PERMISSION);
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == REQUEST_CODE_OVERLAY_PERMISSION) {
            if (Settings.canDrawOverlays(this)) {
                showFloatingWindow();
            } else {
                Toast.makeText(this, "悬浮窗权限未开启", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void showFloatingWindow() {
        // Initialize window manager
        windowManager = (WindowManager) getSystemService(WINDOW_SERVICE);

        // Inflate the floating view layout
        floatingView = LayoutInflater.from(this).inflate(R.layout.floating_view, null);

        // Set up the parameters for the floating window
        params = new WindowManager.LayoutParams(
                WindowManager.LayoutParams.WRAP_CONTENT,
                WindowManager.LayoutParams.WRAP_CONTENT,
                Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ?
                        WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY :
                        WindowManager.LayoutParams.TYPE_PHONE,
                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
                WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
        );
        params.gravity = Gravity.TOP | Gravity.START;
        params.x = 0;
        params.y = 100;

        // Add the view to the window
        windowManager.addView(floatingView, params);

        // Set up touch listener for dragging the floating view
        floatingView.setOnTouchListener((v, event) -> {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    params.x = (int) event.getRawX();
                    params.y = (int) event.getRawY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    params.x = (int) event.getRawX() - (int) event.getX();
                    params.y = (int) event.getRawY() - (int) event.getY();
                    windowManager.updateViewLayout(floatingView, params);
                    break;
            }
            return true;
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        // Remove the floating view when the activity is destroyed
        if (floatingView != null && windowManager != null) {
            windowManager.removeView(floatingView);
        }
    }
}

3. floating_view.xml

创建一个简单的悬浮窗布局文件 res/layout/floating_view.xml

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="悬浮窗示例"
    android:textSize="16sp"
    android:padding="10dp"
    android:background="#FF00FF" />

4. activity_main.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">

    <Button
        android:id="@+id/btnShowFloatingWindow"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="显示悬浮窗" />

</LinearLayout>