Android 权限封装

804 阅读14分钟

简介

应用权限有助于保护对受限数据(例如系统状态和用户的联系信息)和受限操作(例如连接到已配对的设备并录制音频)的访问/执行权限,从而为保护用户隐私提供支持。

权限类型

Android 将权限分为不同的类型,包括安装时权限、运行时权限和特殊权限。每种权限类型都指明了当系统授予应用该权限后,应用可以访问的受限数据范围以及应用可以执行的受限操作范围。每项权限的保护级别取决于其类型,见于权限 API 参考文档

安装时权限

图 2. 某应用商店中显示的某个应用的安装时权限列表.png

安装时权限授予应用对受限数据的受限访问权限,或允许应用执行对系统或其他应用只有最低影响的受限操作。如果您在应用中声明了安装时权限,应用商店会在用户查看应用详情页面时向其显示安装时权限通知,如图 2 所示。系统会在用户安装您的应用时自动向您的应用授予权限。

Android 提供多个安装时权限子类型,包括一般权限和签名权限。

  • 一般权限

此类权限允许访问超出应用沙盒的数据和执行超出应用沙盒的操作,但对用户隐私和其他应用的运行构成的风险很小。

系统会为一般权限分配 normal 保护级别。

  • 签名权限

只有当应用与定义权限的应用或 OS 使用相同的证书签名时,系统才会向应用授予签名权限。

实现特权服务(如自动填充或 VPN 服务)的应用也会使用签名权限。这些应用需要服务绑定签名权限,以便只有系统可以绑定到服务。

运行时权限

图 3. 当应用请求运行时权限时显示的系统权限提示.png

运行时权限也称为危险权限,此类权限授予应用对受限数据的额外访问权限,或允许应用执行对系统和其他应用具有更严重影响的受限操作。因此,您需要先在应用中请求运行时权限,然后才能访问受限数据或执行受限操作。请勿假定这些权限之前已经授予过,务必仔细检查,并根据需要在每次访问之前请求这些权限。

当应用请求运行时权限时,系统会显示运行时权限提示,如图 3 所示。

许多运行时权限会访问用户私人数据,这是一种特殊的受限数据,其中包括可能比较敏感的信息。例如,位置信息和联系信息就属于用户私人数据。

麦克风和摄像头可用于获取特别敏感的信息。因此,该系统会帮助您说明应用获取这类信息的原因

系统会为运行时权限分配 dangerous 保护级别。

特殊权限

特殊权限与特定的应用操作相对应。只有平台和原始设备制造商 (OEM) 可以定义特殊权限。此外,如果平台和 OEM 想要防止有人执行功能特别强大的操作(例如通过其他应用绘图),通常会定义特殊权限。

系统设置中的特殊应用访问权限页面包含一组用户可切换的操作。其中的许多操作都是以特殊权限的形式实现的。

详细了解如何请求特殊权限

系统会为特殊权限分配 appop 保护级别。

权限组

权限可以属于权限组。 权限组由一组逻辑相关的权限组成。例如,发送和接收短信的权限可能属于同一组,因为它们都涉及应用与短信的互动。

权限分组是为了更好地管理和控制应用程序的权限。每个权限分组包含一组相关的权限,用户在授予某个分组的权限时,会同时授予该分组内所有权限。以下是一些常见的权限分组及其包含的权限:

权限分组权限名称权限释义
日历权限READ_CALENDAR读取日历事件
WRITE_CALENDAR写入或修改日历事件
相机权限CAMERA访问设备的摄像头
联系人权限READ_CONTACTS读取联系人信息
WRITE_CONTACTS写入或修改联系人信息
GET_ACCOUNTS访问设备上的账户列表
位置权限ACCESS_FINE_LOCATION访问精确位置
ACCESS_COARSE_LOCATION访问大致位置
麦克风权限RECORD_AUDIO录制音频
电话权限READ_PHONE_STATE读取电话状态
CALL_PHONE拨打电话
READ_CALL_LOG读取通话记录
WRITE_CALL_LOG写入或修改通话记录
ADD_VOICEMAIL添加语音邮件
USE_SIP使用SIP视频服务
PROCESS_OUTGOING_CALLS处理拨出电话
传感器权限BODY_SENSORS访问身体传感器数据
短信权限SEND_SMS发送短信
RECEIVE_SMS接收短信
READ_SMS读取短信内容
RECEIVE_WAP_PUSH接收WAP推送消息
RECEIVE_MMS接收彩信
存储权限READ_EXTERNAL_STORAGE读取外部存储
WRITE_EXTERNAL_STORAGE写入外部存储

请求运行时权限

如果您确定您的应用需要声明和请求运行时权限,请完成以下步骤:

  1. 在应用的清单文件中,声明应用可能需要请求的权限

  2. 设计应用的用户体验,使应用中的特定操作与特定运行时权限相关联。告知用户哪些操作可能会要求他们向您的应用授予访问其私人数据的权限。

  3. 等待用户调用应用中需要访问特定用户私人数据的任务或操作。届时,您的应用可以请求访问相应数据所需的运行时权限。

  4. 检查用户是否已授予应用所需的运行时权限。如果已授权,那么您的应用可以访问用户私人数据。如果没有,请继续执行下一步。

    每次执行需要该权限的操作时,您都必须检查自己是否具有该权限。

  5. 检查您的应用是否应向用户显示理由,说明您的应用需要用户授予特定运行时权限的原因。如果系统确定您的应用不应显示理由,请继续直接执行下一步,无需显示界面元素。

    不过,如果系统确定您的应用应该显示一个理由,请在界面元素中向用户显示理由,明确说明您的应用试图访问哪些数据,以及应用获得运行时权限后可为用户提供哪些好处。用户确认理由后,请继续执行下一步。

  6. 请求您的应用访问用户私人数据所需的运行时权限。系统会显示运行时权限提示,例如权限概览页面上显示的提示。

  7. 检查用户的响应,他们可能会选择同意或拒绝授予运行时权限。

  8. 如果用户向您的应用授予相应权限,您就可以访问用户私人数据。如果用户拒绝授予该权限,请适当降低应用体验,使应用在未获得受该权限保护的信息时也能向用户提供功能。

workflow-runtime.svg

代码示例

package com.dafay.demo.permission

import android.Manifest
import android.content.pm.PackageManager
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.dafay.demo.lab.base.base.BaseActivity
import com.dafay.demo.lib.base.base.notification.showSnackbar
import com.dafay.demo.lib.base.utils.info
import com.dafay.demo.permission.databinding.ActivitySinglePermissionBinding
import com.dafay.demo.permission.utils.showAlertDialog

class TestPermissionActivity : BaseActivity<ActivitySinglePermissionBinding>(ActivitySinglePermissionBinding::inflate) {

    private val rationaleTitle = "Permission requests"
    private val rationaleMsg = "In order to provide a better service experience, our app requires access to your camera permissions. Specifically, balabala..."

    private val requestPermissionLauncher =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
            if (isGranted) {
                info("Permission: Granted")
                binding.clContainer.showSnackbar("Permission: Granted", "ok") {}
            } else {
                info("Permission: Denied")
                binding.clContainer.showSnackbar("Permission: Denied", "ok") {}
            }
        }

    override fun initViews() {
        super.initViews()
        binding.btnRequestPermission.setOnClickListener {
            onRequestPermission(Manifest.permission.CAMERA)
        }
    }

    private fun onRequestPermission(permission: String) {
        if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
            info("checkSelfPermission: granted")
            binding.clContainer.showSnackbar("checkSelfPermission -> permission_granted", "ok") {}
            return
        }
        // 是否应该显示申请权限的理由(弹窗或其它引导)
        if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
            showAlertDialog(rationaleTitle, rationaleMsg, "确定", "取消", {
                info("requestPermissionLauncher.launch")
                requestPermissionLauncher.launch(permission)
            })
            return
        }
        info("requestPermissionLauncher.launch")
        requestPermissionLauncher.launch(permission)
    }
}

权限工作流的更多示例应用,请参考 GitHub 上的 Android 权限示例仓库

运行时权限的封装

实际项目中,往往需要我们在 Fragment/Activity 中进行运行时权限处理,有些场景需要对多权限进行动态请求,通过封装,减轻了编写一堆检查语句来判断您是否获得权限的负担,以保证您的代码干净、安全。优秀的开源库有 PermissionsDispatcherRxPermissions 等等。

下面代码,借鉴 PermissionsDispatcher,进行简单封装。

package com.dafay.demo.permission

import android.Manifest
import android.widget.Toast
import com.dafay.demo.lab.base.base.BaseActivity
import com.dafay.demo.permission.databinding.ActivitySinglePermissionBinding
import com.dafay.demo.permission.utils.PermissionRequest
import com.dafay.demo.permission.utils.PermissionsRequester
import com.google.android.material.dialog.MaterialAlertDialogBuilder

class TestPermissionActivity :
    BaseActivity<ActivitySinglePermissionBinding>(ActivitySinglePermissionBinding::inflate) {
		// 使用 PermissionsRequester 对动态权限进行处理
    lateinit var permissionsManager: PermissionsRequester

    override fun initViews() {
        super.initViews()

        permissionsManager = PermissionsRequester(
            Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.CAMERA,
            activity = this,
            onShowRationale = ::dealShowRationale,
            onPermissionDenied = ::dealPermissionDenied,
            onNeverAskAgain = ::dealNeverAskAgain,
            requiresPermission = ::dealRequiresPermission
        )

        binding.btnRequestPermission.setOnClickListener {
            permissionsManager.launch()
        }
    }

    private fun dealPermissionDenied() {
        Toast.makeText(this, "Some permission was denied", Toast.LENGTH_SHORT).show()
    }

    private fun dealRequiresPermission() {
        Toast.makeText(this, "All permissions Granted", Toast.LENGTH_SHORT).show()
    }

    private fun dealShowRationale(request: PermissionRequest) {
        MaterialAlertDialogBuilder(this)
            .setTitle("Permission Required")
            .setMessage("This app needs the Camera permission to take photos. Location permission to ...")
            .setPositiveButton("OK") { dialog, which ->
                request.proceed()
            }
            .setNegativeButton("Cancel") { dialog, which ->
                request.cancel()
            }
            .show()
    }

    private fun dealNeverAskAgain() {
        Toast.makeText(this, "Some permission was denied with never ask again.", Toast.LENGTH_SHORT).show()
    }
}

PermissionsRequester 对请求权限的各种结果进行封装,在 PermissionsRequester 里通过代理 Fragment 对动态权限进行申请。

package com.dafay.demo.permission.utils

import androidx.fragment.app.FragmentActivity

internal typealias Fun = () -> Unit
internal typealias ShowRationaleFun = (PermissionRequest) -> Unit

/**
 * 弹窗弹出时,PermissionRequest 作为参数传递给弹窗,以便链接到后续的流程
 */
interface PermissionRequest {
    fun proceed()
    fun cancel()
}

internal class KtxPermissionRequest(
    private val requestPermission: Fun,
    private val permissionDenied: Fun?
) : PermissionRequest {
    override fun proceed() {
        requestPermission.invoke()
    }

    override fun cancel() {
        permissionDenied?.invoke()
    }

    companion object {
        fun create(onPermissionDenied: Fun?, requestPermission: Fun) = KtxPermissionRequest(
            requestPermission = requestPermission,
            permissionDenied = onPermissionDenied
        )
    }
}

/**
 * 包装回调,打开代理 fragment 进行权限请求
 */
class PermissionsRequester(
    vararg val permissions: String,
    private val activity: FragmentActivity,
    private val onShowRationale: ShowRationaleFun?,
    private val onPermissionDenied: Fun?,
    private val requiresPermission: Fun,
    onNeverAskAgain: Fun?,
) {
    private val requestFun: Fun = {
        activity.supportFragmentManager
            .beginTransaction()
            .replace(
                android.R.id.content,
                PermissionRequestFragment.NormalRequestPermissionFragment.newInstance(permissions)
            )
            .commitAllowingStateLoss()
    }

    init {
        PermissionRequestFragment.permissionRequestWrapper = object : PermissionResultInterface {
            override fun requiresPermission() {
                requiresPermission.invoke()
            }

            override fun onPermissionDenied(key: Array<String>, grantResults: IntArray) {
                onPermissionDenied?.invoke()
            }

            override fun onNeverAskAgain(key: Array<String>, grantResults: IntArray) {
                onNeverAskAgain?.invoke()
            }
        }
    }

    fun launch() {
        if (PermissionUtils.hasSelfPermissions(
                activity,
                *permissions
            )
        ) {
            requiresPermission()
        } else {
            if (PermissionUtils.shouldShowRequestPermissionRationale(
                    activity,
                    *permissions
                ) && onShowRationale != null
            ) {
                onShowRationale.invoke(KtxPermissionRequest.create(onPermissionDenied, requestFun))
            } else {
                requestFun.invoke()
            }
        }
    }
}

在 Activity、Fragment 都能使用,对权限请求通过一个无页面的 Fragment 进行代理,这种方式在 JetPack 的 Lifecycle 也有运用。

package com.dafay.demo.permission.utils

import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import java.util.Random

/**
 * 权限请求结果的回调
 */
interface PermissionResultInterface {
    fun requiresPermission()
    fun onPermissionDenied(key: Array<String>, grantResults: IntArray)
    fun onNeverAskAgain(key: Array<String>, grantResults: IntArray)
}

/**
 * 使用代理 fragment,统一处理来自 act/frg 的请求
 */
internal sealed class PermissionRequestFragment : Fragment() {
    protected val requestCode = Random().nextInt(1000)

    companion object {
        var permissionRequestWrapper: PermissionResultInterface? = null
    }

    protected fun dismiss() =
        fragmentManager?.beginTransaction()?.remove(this)?.commitAllowingStateLoss()

    internal class NormalRequestPermissionFragment : PermissionRequestFragment() {
        private val TAG = PermissionRequestFragment::class.java.simpleName

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            val permissions = arguments?.getStringArray(BUNDLE_PERMISSIONS_KEY) ?: return
            requestPermissions(permissions, requestCode)
        }

        override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<String>,
            grantResults: IntArray
        ) {
            super.onRequestPermissionsResult(requestCode, permissions, grantResults)
            if (requestCode == this.requestCode) {
                if (PermissionUtils.verifyPermissions(*grantResults)) {
                    permissionRequestWrapper?.requiresPermission()
                } else {
                    if (!PermissionUtils.shouldShowRequestPermissionRationale(
                            this,
                            *permissions
                        )
                    ) {
                        permissionRequestWrapper?.onNeverAskAgain(permissions, grantResults)
                    } else {
                        permissionRequestWrapper?.onPermissionDenied(permissions, grantResults)
                    }
                }
            }
            dismiss()
        }

        override fun onDestroy() {
            super.onDestroy()
            Log.i(TAG, "======> NormalRequestPermissionFragment onDestroy");
        }

        companion object {
            const val BUNDLE_PERMISSIONS_KEY = "key:permissions"

            fun newInstance(permissions: Array<out String>) =
                NormalRequestPermissionFragment().apply {
                    arguments = Bundle().apply {
                        putStringArray(BUNDLE_PERMISSIONS_KEY, permissions)
                    }
                }
        }
    }

}

这样通过几个类,即完成了运行时权限的简单封装,但这种代码只能在 demo 程序中提供些许的便利。对于正式的项目,这种封装太过简陋,页面生命周期的改变(例如屏幕旋转)、不同 Android 系统版本的兼容、特殊权限的处理等等都没有考虑。PermissionsDispatcher 这样的开源库看似简单,做到 PermissionsDispather 这样完善,需要比我们直观看起来多出十几倍的知识量,并需要非凡的耐心。

扩展知识

  • 权限的三种选项

    在 Android 中,当应用请求权限时,用户通常会看到以下三种选项:

    1. 仅在使用该应用时允许

      • 该选项允许应用在前台运行时访问所请求的权限。例如,如果应用需要访问位置信息,只有在应用处于活动状态时才能获取位置信息。
      • 一旦应用进入后台或被关闭,权限将被撤销,应用将无法继续访问该权限。
    2. 仅限这一次

      • 该选项允许应用在当前会话中访问所请求的权限,但仅限于这一次使用。
      • 当用户关闭应用或重新启动应用时,权限将被撤销,应用需要再次请求权限。
    3. 不允许

      • 该选项拒绝应用访问所请求的权限。
      • 应用将无法访问该权限,除非用户在设置中手动更改权限设置。

    这些选项旨在增强用户对隐私的控制,使用户能够根据自己的需求和信任程度来管理应用的权限访问。如果你有特定的权限或使用场景需要进一步解释,请告诉我,我可以提供更多详细信息。

  • 权限版本差异

    Android系统在不同版本中对权限管理进行了多次调整和改进。以下是一些主要版本的权限变化:

    1. Android 5.1及之前

      • 权限在安装时授予,用户在安装应用时会看到所有权限请求。
      • 一旦安装完成,应用可以访问所有声明的权限。
    2. Android 6.0 (API 23)

      • 引入了运行时权限,用户需要在应用运行时动态授予权限。
      • 权限分为普通权限和危险权限,危险权限需要用户明确同意。
    3. Android 7.0 (API 24)

      • 增加了对私有目录访问的限制,应用不能通过**file://** URI访问其他应用的私有目录文件。
      • 需要使用**FileProvider**来共享文件。
    4. Android 8.0 (API 26)

      • 修复了权限组的问题,系统只会授予应用明确请求的权限。
      • 一旦用户授予某个权限,后续对同一权限组中其他权限的请求将自动批准。
    5. Android 10 (API 29)

      • 引入了作用域存储,限制应用对外部存储的访问。
      • 应用只能访问自己创建的文件,其他文件需要通过特定的API访问。
    6. Android 11 (API 30)

      • 增加了对外部存储访问的限制,应用需要使用**MANAGE_EXTERNAL_STORAGE**权限来访问所有文件。
      • 引入了一次性权限,用户可以授予应用一次性访问权限。
    7. Android 12 (API 31)

      • 增加了麦克风和摄像头的指示灯,用户可以看到应用何时使用这些硬件。
      • 引入了新的权限请求对话框,用户可以选择仅在应用使用时授予权限。
    8. Android 13 (API 33)

      • 增加了对通知权限的管理,应用需要请求用户同意才能发送通知。
      • 引入了照片选择器,用户可以选择授予应用访问特定照片的权限。

    这些变化旨在提高用户隐私和安全性,同时也增加了开发者在处理权限时的复杂性。

  • 版本编译说明

    在Android开发中,系统版本和应用编译版本之间的关系主要通过以下三个关键属性来管理:compileSdkVersionminSdkVersiontargetSdkVersion。它们分别定义了应用的编译环境、最低支持的系统版本以及目标系统版本。

    1. compileSdkVersion

      • 这是应用编译时使用的SDK版本。它决定了你可以使用的API和编译器功能。
      • 建议使用最新的SDK版本进行编译,以便利用最新的功能和优化。
      • 修改**compileSdkVersion**不会改变应用的运行时行为,只会影响编译时的检查和警告。
    2. minSdkVersion

      • 定义了应用可以运行的最低Android系统版本。低于这个版本的设备将无法安装该应用。
      • 这个值决定了应用的兼容性范围。设置较低的**minSdkVersion**可以覆盖更多的设备,但可能需要处理更多的兼容性问题。
    3. targetSdkVersion

      • 表示应用已经在该版本上进行了测试,并且开发者希望应用在该版本上表现最佳。
      • 当系统版本高于**targetSdkVersion**时,系统会启用向后兼容模式,以确保应用的行为与预期一致。
      • 建议尽量将**targetSdkVersion**设置为最新的版本,以便利用最新的系统特性和优化。

    这三个属性之间的关系如下:

    • minSdkVersion <= targetSdkVersion <= compileSdkVersion

    例如,如果你的应用设置如下:

    android {
        compileSdkVersion 33
        defaultConfig {
            minSdkVersion 21
            targetSdkVersion 33
        }
    }
    

    这意味着你的应用使用 API Level 33 进行编译,支持最低 API Level 21 的设备,并且在 API Level 33 上进行了测试和优化。

  • adb 查看危险权限

    adb shell pm list permissions -g -d
    Dangerous Permissions:
    
    group:com.google.android.gms.permission.CAR_INFORMATION
      permission:com.google.android.gms.permission.CAR_VENDOR_EXTENSION
      permission:com.google.android.gms.permission.CAR_MILEAGE
      permission:com.google.android.gms.permission.CAR_FUEL
    
    group:android.permission-group.CONTACTS
    
    group:android.permission-group.PHONE
    
    group:android.permission-group.CALENDAR
    
    group:android.permission-group.CALL_LOG
    
    group:android.permission-group.CAMERA
    
    group:android.permission-group.READ_MEDIA_VISUAL
    
    group:android.permission-group.READ_MEDIA_AURAL
    
    group:android.permission-group.UNDEFINED
      permission:android.permission.READ_SMS
      permission:android.permission.READ_CALENDAR
      permission:android.permission.POST_NOTIFICATIONS
      permission:android.permission.READ_CALL_LOG
      permission:android.permission.ACCESS_FINE_LOCATION
      permission:android.permission.ANSWER_PHONE_CALLS
      permission:android.permission.RECEIVE_WAP_PUSH
      permission:android.permission.BODY_SENSORS
      permission:android.permission.READ_PHONE_NUMBERS
      permission:android.permission.NEARBY_WIFI_DEVICES
      permission:android.permission.RECEIVE_MMS
      permission:android.permission.RECEIVE_SMS
      permission:android.permission.BLUETOOTH_CONNECT
      permission:android.permission.READ_EXTERNAL_STORAGE
      permission:android.permission.ACCESS_COARSE_LOCATION
      permission:android.permission.READ_PHONE_STATE
      permission:android.permission.SEND_SMS
      permission:android.permission.CALL_PHONE
      permission:android.permission.READ_MEDIA_IMAGES
      permission:android.permission.WRITE_CONTACTS
      permission:android.permission.BODY_SENSORS_BACKGROUND
      permission:android.permission.ACCEPT_HANDOVER
      permission:android.permission.CAMERA
      permission:android.permission.WRITE_CALENDAR
      permission:android.permission.WRITE_CALL_LOG
      permission:android.permission.READ_MEDIA_AUDIO
      permission:android.permission.READ_MEDIA_VIDEO
      permission:android.permission.USE_SIP
      permission:android.permission.PROCESS_OUTGOING_CALLS
      permission:android.permission.READ_CELL_BROADCASTS
      permission:android.permission.BLUETOOTH_ADVERTISE
      permission:android.permission.GET_ACCOUNTS
      permission:android.permission.WRITE_EXTERNAL_STORAGE
      permission:android.permission.UWB_RANGING
      permission:android.permission.ACTIVITY_RECOGNITION
      permission:android.permission.RECORD_AUDIO
      permission:android.permission.READ_CONTACTS
      permission:android.permission.ACCESS_BACKGROUND_LOCATION
      permission:android.permission.BLUETOOTH_SCAN
      permission:android.permission.ACCESS_MEDIA_LOCATION
      permission:com.android.voicemail.permission.ADD_VOICEMAIL
    
    group:android.permission-group.ACTIVITY_RECOGNITION
    
    group:android.permission-group.SENSORS
    
    group:android.permission-group.LOCATION
      permission:com.google.android.gms.permission.CAR_SPEED
    
    group:android.permission-group.STORAGE
    
    group:android.permission-group.NOTIFICATIONS
    
    group:android.permission-group.MICROPHONE
    
    group:android.permission-group.NEARBY_DEVICES
    
    group:android.permission-group.SMS
    
    ungrouped:
      permission:com.google.android.providers.talk.permission.WRITE_ONLY
      permission:com.google.android.gm.permission.READ_CONTENT_PROVIDER
      permission:com.google.android.providers.talk.permission.READ_ONLY
    

参考文档&资料

原文链接 (demo-permission)

Android 中的权限(google)

三态位置权限

使用 Kotlin 向应用添加运行时权限(codelabs)

Android 隐私保护 Codelab