简介
应用权限有助于保护对受限数据(例如系统状态和用户的联系信息)和受限操作(例如连接到已配对的设备并录制音频)的访问/执行权限,从而为保护用户隐私提供支持。
权限类型
Android 将权限分为不同的类型,包括安装时权限、运行时权限和特殊权限。每种权限类型都指明了当系统授予应用该权限后,应用可以访问的受限数据范围以及应用可以执行的受限操作范围。每项权限的保护级别取决于其类型,见于权限 API 参考文档。
安装时权限
附件未导入:图 2. 某应用商店中显示的某个应用的安装时权限列表。
安装时权限授予应用对受限数据的受限访问权限,或允许应用执行对系统或其他应用只有最低影响的受限操作。如果您在应用中声明了安装时权限,应用商店会在用户查看应用详情页面时向其显示安装时权限通知,如图 2 所示。系统会在用户安装您的应用时自动向您的应用授予权限。
Android 提供多个安装时权限子类型,包括一般权限和签名权限。
- 一般权限
此类权限允许访问超出应用沙盒的数据和执行超出应用沙盒的操作,但对用户隐私和其他应用的运行构成的风险很小。
系统会为一般权限分配 normal 保护级别。
- 签名权限
只有当应用与定义权限的应用或 OS 使用相同的证书签名时,系统才会向应用授予签名权限。
实现特权服务(如自动填充或 VPN 服务)的应用也会使用签名权限。这些应用需要服务绑定签名权限,以便只有系统可以绑定到服务。
运行时权限也称为危险权限,此类权限授予应用对受限数据的额外访问权限,或允许应用执行对系统和其他应用具有更严重影响的受限操作。因此,您需要先在应用中请求运行时权限,然后才能访问受限数据或执行受限操作。请勿假定这些权限之前已经授予过,务必仔细检查,并根据需要在每次访问之前请求这些权限。
许多运行时权限会访问用户私人数据,这是一种特殊的受限数据,其中包括可能比较敏感的信息。例如,位置信息和联系信息就属于用户私人数据。
麦克风和摄像头可用于获取特别敏感的信息。因此,该系统会帮助您说明应用获取这类信息的原因。
系统会为运行时权限分配 dangerous 保护级别。
特殊权限
特殊权限与特定的应用操作相对应。只有平台和原始设备制造商 (OEM) 可以定义特殊权限。此外,如果平台和 OEM 想要防止有人执行功能特别强大的操作(例如通过其他应用绘图),通常会定义特殊权限。
系统设置中的特殊应用访问权限页面包含一组用户可切换的操作。其中的许多操作都是以特殊权限的形式实现的。
详细了解如何请求特殊权限。
系统会为特殊权限分配 appop 保护级别。
权限分组是为了更好地管理和控制应用程序的权限。每个权限分组包含一组相关的权限,用户在授予某个分组的权限时,会同时授予该分组内所有权限。以下是一些常见的权限分组及其包含的权限:
如果您确定您的应用需要声明和请求运行时权限,请完成以下步骤:
- 在应用的清单文件中,声明应用可能需要请求的权限。
- 设计应用的用户体验,使应用中的特定操作与特定运行时权限相关联。告知用户哪些操作可能会要求他们向您的应用授予访问其私人数据的权限。
- 等待用户调用应用中需要访问特定用户私人数据的任务或操作。届时,您的应用可以请求访问相应数据所需的运行时权限。
- 检查用户是否已授予应用所需的运行时权限。如果已授权,那么您的应用可以访问用户私人数据。如果没有,请继续执行下一步。
每次执行需要该权限的操作时,您都必须检查自己是否具有该权限。
- 检查您的应用是否应向用户显示理由,说明您的应用需要用户授予特定运行时权限的原因。如果系统确定您的应用不应显示理由,请继续直接执行下一步,无需显示界面元素。
不过,如果系统确定您的应用应该显示一个理由,请在界面元素中向用户显示理由,明确说明您的应用试图访问哪些数据,以及应用获得运行时权限后可为用户提供哪些好处。用户确认理由后,请继续执行下一步。
代码示例
package com.dafay.demo.permission import android.Manifestimport android.content.pm.PackageManagerimport androidx.activity.result.contract.ActivityResultContractsimport androidx.core.app.ActivityCompatimport androidx.core.content.ContextCompatimport com.dafay.demo.lab.base.base.BaseActivityimport com.dafay.demo.lib.base.base.notification.showSnackbarimport com.dafay.demo.lib.base.utils.infoimport com.dafay.demo.permission.databinding.ActivitySinglePermissionBindingimport 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 中进行运行时权限处理,有些场景需要对多权限进行动态请求,通过封装,减轻了编写一堆检查语句来判断您是否获得权限的负担,以保证您的代码干净、安全。优秀的开源库有 PermissionsDispatcher、RxPermissions 等等。
下面代码,借鉴 PermissionsDispatcher,进行简单封装。
package com.dafay.demo.permission import android.Manifestimport android.widget.Toastimport com.dafay.demo.lab.base.base.BaseActivityimport com.dafay.demo.permission.databinding.ActivitySinglePermissionBindingimport com.dafay.demo.permission.utils.PermissionRequestimport com.dafay.demo.permission.utils.PermissionsRequesterimport 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 = () -> Unitinternal 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.Bundleimport android.util.Logimport androidx.fragment.app.Fragmentimport 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 这样完善,可能需要比我们直观看起来多出十几倍的知识量,并需要非凡的耐心才能达到。
扩展知识
当应用请求权限时,用户通常会看到以下三种选项
权限版本差异
版本编译说明
adb 查看危险权限
adb shell pm list permissions -g -dDangerous 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