权限大整合

2,544 阅读12分钟

简要

因为目前开发的项目target版本是在32,所以这边整理的内容没有包含android 13的变动,主要涉及到了定位、蓝牙等权限

READ_PHONE_STATE权限

READ_PHONE_STATE权限是允许访问电话状态权限,在android 11之后权限名称更改为READ_PHONE_NUMBERS,所以当目标版本在android11以下还是用READ_PHONE_STATE去动态申请,以 Android 11 或更高版本为目标平台则申请READ_PHONE_NUMBERS。

<manifest>
    <!-- Grants the READ_PHONE_STATE permission only on devices that run
         Android 10 (API level 29) and lower. -->
    <uses-permission android:name="READ_PHONE_STATE"
                     android:maxSdkVersion="29"  />
     <uses-permission android:name="READ_PHONE_NUMBERS" /> 
</manifest>

特别说明:在小米手机MIUI对于该权限有特殊限制,如果需要此权限需要提交申请,或引导用户将应用设置为默认拨号应用,具体可以查看小米官方说明:dev.mi.com/console/doc…

  • 重要提示:以下权限,在MIUI 11 3.12 的开发版后将无法获取:
Manifest.permission.READ_PHONE_STATE

Manifest.permission.READ_PHONE_NUMBERS

Manifest.permission.CALL_PHONE Manifest.permission.ANSWER_PHONE_CALLS

蓝牙权限

android12以下,进行蓝牙扫描时,只需要在清单文件上声明以下normal级权限即可(特别说明:需要有位置权限):

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

在target为android12的版本中开发时,蓝牙扫描是需要进行权限申请的,具体蓝牙权限如下(无需位置权限):

<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

如果您的应用与蓝牙设备互动,强烈建议您执行以下操作:

  1. 如果您的应用查找蓝牙设备(如蓝牙低功耗 (BLE) 外围设备),请向应用的清单中添加 BLUETOOTH_SCAN 权限。
  2. 如果您的应用使当前设备可被其他蓝牙设备检测到,请向应用的清单中添加 BLUETOOTH_ADVERTISE 权限。
  3. 如果您的应用与已配对的蓝牙设备通信,请向应用的清单中添加 BLUETOOTH_CONNECT 权限。
  4. 对于旧版蓝牙相关的权限声明,请将 android:maxSdkVersion 设为 30。此应用兼容性步骤有助于系统仅向您的应用授予在搭载 Android 12 的设备上安装时所需的蓝牙权限。

以下代码段演示了如何在您的应用中声明这些新权限:

<manifest>
    <!-- Request legacy Bluetooth permissions on older devices. -->
    <uses-permission android:name="android.permission.BLUETOOTH"
                     android:maxSdkVersion="30" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
                     android:maxSdkVersion="30" />

    <!-- Needed only if your app looks for Bluetooth devices.
         You must add an attribute to this permission, or declare the
         ACCESS_FINE_LOCATION permission, depending on the results when you
         check location usage in your app. -->
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />

    <!-- Needed only if your app makes the device discoverable to Bluetooth
         devices. -->
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />

    <!-- Needed only if your app communicates with already-paired Bluetooth
         devices. -->
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    ...
</manifest>

定位权限

<!-- Allows an app to access approximate location.  近似定位权限,api1,如:网络定位 -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- Allows an app to access precise location 精准定位权限,api1,如:GPS定位-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

<!-- Allows an app to access location in the background. 后台定位权限,api29,android10新增 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

android6

Android 10 之前只有ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION;正常申请即可

android10

Android 10 新增加了后台定位权限:ACCESS_BACKGROUND_LOCATION,该权限对应始终允许;老的权限:ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION代表仅前台使用允许;

应用的targetSdkVersion<Q,谷歌提供了兼容性方案,只要应用申请了老的位置权限ACCESS_FINE_LOCATION或者ACCESS_COARSE_LOCATION,会默认请求ACCESS_BACKGROUND_LOCATION权限,动态授权弹框参考下面第一个图。

应用的TargetSdkVersion>=Q,如果应用必须要始终定位,可以只申请ACCESS_BACKGROUND_LOCATION即可,权限弹框参考下面第三个图;如果应用只需要申请前台定位,则只需要申请老的定位权限即可,具体授权弹框参考第二个图。如果都申请则出现三态权限弹框,参考下面第一个图。

img

img

img

android11

argetSdkVersion=android11 时,不能同时申请普通定位权限和后台定位权限。argetSdkVersion=android11,在android11手机上,同时申请两个权限将直接授权失败,不会有弹窗提示。

requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_BACKGROUND_LOCATION), 100)

解决方案:只有成功申请了 普通定位权限,才能申请后台定位权限。

\1) 先申请前台位置信息访问权限;

\2) 再申请后台位置信息访问权限,引导用户到设置中进行授予。

android12

Android12新增模糊定位功能,使用户可以更加细粒度的管控设备位置权限。

需要注意的是,Android12要求必须同时请求ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION权限才会触发相关权限弹框,如果只请求ACCESS_COARSE_LOCATION权限,系统将只会弹出请求大致位置的弹框。如果开发者只请求ACCESS_FINE_LOCATION,系统会忽略该请求,并在Logcat里记录错误信息:ACCESS_FINE_LOCATION must be requested with ACCESS_COARSE_LOCATION(测试时直接申请确切位置权限也是可以的)。开发者在适配Android12应用时一定要注意上述权限请求相关要求。

img

在 Android 12 和更高版本中,用户可以转到系统设置,以设置任何应用的首选位置信息精确度,而不管该应用的目标 SDK 版本是什么。即使您的应用安装在搭载 Android 11 或更低版本的设备上,用户随后又将该设备升级到 Android 12 或更高版本,也是如此。

**注意: **如果用户从权限对话框或在系统设置中将应用的位置信息使用权从确切位置降级到大致位置,系统会重启应用的进程。

android%40122png

大致位置

提供设备位置的估算值,将范围限定在大约 1.6 公里(1 英里)内。当您声明 ACCESS_COARSE_LOCATION 权限(而非 ACCESS_FINE_LOCATION 权限)时,您的应用会使用这种级别的位置信息精确度。

确切位置

提供尽可能准确的设备位置估算值,通常将范围限定在大约 50 米(160 英尺)内,有时精确到几米(10 英尺)范围以内。当您声明 ACCESS_FINE_LOCATION 权限时,您的应用会使用这种级别的位置信息精确度。

如果用户授予大致位置信息权限,您的应用只能获取大致位置信息(无论它声明了哪些位置信息权限)。

当用户仅授予大致位置信息使用权时,您的应用应该仍会正常工作。如果应用中的某项功能确实需要使用 ACCESS_FINE_LOCATION 权限访问确切位置,您可以请求用户允许该应用获取确切位置信息。

存储权限

存储基本知识

先来看看存储区域划分:

imagepng

其中,以下目录无需存储权限即可访问:

1、App自身的内部存储 2、App自身的自带外部存储-私有目录

剩下的都需要申请存储权限,Android 10.0前后对于存储作用域访问的区别就体现在如何访问剩余这些目录内的文件。

重点在自带外部存储之共享存储空间和其它目录

android6

申请危险权限,即可正常读写

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

android10

分区存储,在Android10就已经推行了,简单的说,就是应用对于文件的读写只能在沙盒环境,也就是属于自己应用的目录里面读写。其他媒体文件可以通过MediaStore进行访问。

targetSdkVersion = 29应用中,设置android:requestLegacyExternalStorage="true",就可以不启动分区存储,让以前的文件读取正常使用。

android11

分区存储强制执行

清单权限

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

从 Android 11 开始,应用无法在外部存储设备上创建自己的应用专用目录。如需访问系统为您的应用提供的目录,请调用 getExternalFilesDirs()

公有共享目录

  • Image: MediaStore.Images.Media.EXTERNAL_CONTENT_URI
  • Video: MediaStore.Video.Media.EXTERNAL_CONTENT_URI
  • Audio: MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
  • Downloads: MediaStore.Downloads.EXTERNAL_CONTENT_URI

android11之后,不能使用File相关的api进行文件操作,android提供了MediaStore API进行文件的操作,一个 文本文件 , 只能存储在 Download 和 Documents 目录下 , Download 目录可以存放任何类型的文件 , Documents 目录只能存储文本文件 。

一个在downloads目录下文件存储操作:

创建文件并写入数据

 val contentResolver = context.contentResolver
        //Uri uri = MediaStore.Files.getContentUri("external");
        val uri = MediaStore.Downloads.EXTERNAL_CONTENT_URI
        val values = ContentValues()
        val path = Environment.DIRECTORY_DOWNLOADS + "/device1"
        values.put(MediaStore.Downloads.RELATIVE_PATH, path)
        values.put(MediaStore.Downloads.DISPLAY_NAME, "devicesn1.text")
        values.put(MediaStore.Downloads.TITLE, path)
        val resultUri = contentResolver.insert(uri, values)
        try {
            val outputStream = contentResolver.openOutputStream(resultUri!!)
            //将字符串转成字节
            val contentInBytes = content.toByteArray()
            outputStream!!.write(contentInBytes)
            outputStream.flush()
            outputStream.close()
            Log.i(TAG, "插入sn内容成功: $content")
            //Toast.makeText(MainActivity.this, "插入成功", Toast.LENGTH_LONG).show();
        } catch (e: Exception) {
            e.printStackTrace()
            //失败
            Log.e(TAG, "存储sn失败:" + e.message)
        }

读数据

 var content: String? = ""
        val extnerl = MediaStore.Downloads.EXTERNAL_CONTENT_URI
        Log.i(TAG, "Fileuri:$extnerl")
        val selection = MediaStore.Downloads.DISPLAY_NAME + "=?"
        val args = arrayOf("devicesn1.text")
        val projections = arrayOf(MediaStore.Downloads._ID)
        val cursor = context.contentResolver.query(extnerl, projections, selection, args, null)
        if (cursor!!.moveToFirst()) {
            val queryUir = ContentUris.withAppendedId(extnerl, cursor.getLong(0))
            //Toast.makeText(MainActivity.this, "查询success" + queryUir, Toast.LENGTH_LONG).show();
            Log.i(TAG, "查询success$queryUir")
            cursor.close()
            val contentResolver = context.contentResolver
            var inputStream: InputStream? = null
            try {
                inputStream = contentResolver.openInputStream(queryUir)
                val buffer = ByteArray(1024)
                var len = 0
                val bos = ByteArrayOutputStream()
                while (inputStream!!.read(buffer).also { len = it } != -1) {
                    bos.write(buffer, 0, len)
                }
                bos.close()
                val str = String(bos.toByteArray())
                content = str
                //tv_find_file.setText(str);
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

android13 细化的媒体权限

从Android 13开始,如果你的应用targetSdk指定到了33或以上,那么READ_EXTERNAL_STORAGE权限就完全失去了作用,申请它将不会产生任何的效果。

与此相对应地,Google新增了READ_MEDIA_IMAGES、READ_MEDIA_VIDEO和READ_MEDIA_AUDIO这3个运行时权限,分别用于管理手机的照片、视频和音频文件。

也就是说,以前只要申请一个READ_EXTERNAL_STORAGE权限就可以了。现在不行了,得按需申请,用户从而能够更加精细地了解你的应用到底申请了哪些媒体权限。

Android 14

选择照片

为了能够更好地保护用户隐私, Google在Android 14系统中新增了选择性照片和视频访问授权功能。

那么什么是选择性照片和视频访问授权呢?

在过去,当一个App申请了READ_MEDIA_IMAGES权限,如果用户选择了同意,那么该App就可以访问这台手机上所有的照片。用户是没有办法限制该App只能访问特定的某几张照片的。

而Android 14新增的这个功能则允许用户选择,是一次性授权该App访问所有的照片,还是只能访问几张特定的照片。视频也是同样的道理。

Android 14推出了一个全新的运行时权限,也就是选择性照片和视频访问权限:READ_MEDIA_VISUAL_USER_SELECTED

android.permission.READ_MEDIA_VISUAL_USER_SELECTED
前台服务

在 Android 14(API Level 34)及更高版本中,Google 对前台服务(Foreground Services)进行了严格分类。声明 android:foregroundServiceType 是为了明确告知系统该服务的具体用途,系统会根据类型应用不同的权限限制和运行规则。

connectedDevicedataSyncmediaProcessing 的核心区别如下:

特性 / 类型connectedDevicedataSyncmediaProcessing
核心用途与已连接的外部硬件设备通信与远程服务器或网络同步/上传/下载数据对媒体文件进行耗时的本地计算/处理
典型场景蓝牙/Wi-Fi 连接手环、车载设备、外接传感器云端同步相册、下载大文件、备份数据库视频转码、音频裁剪、滤镜渲染、格式转换
系统权限要求依赖具体的硬件权限(如 BLUETOOTH_CONNECT, CHANGE_NETWORK_STATE 等)需声明 FOREGROUND_SERVICE_DATA_SYNC 权限需声明 FOREGROUND_SERVICE_MEDIA_PROCESSING 权限
系统限制与超时通常适合长连接,无严格短超时限有严格系统限制(通常最多运行数小时),超时会被系统停止适合有限任务,运行过长可能触发系统审查或被终止
替代方案建议无明显替代(长连接必须使用)优先推荐 WorkManager(非即时/后台同步)优先推荐 WorkManager(耗时且无需即时前台交互)
详细解析
  1. connectedDevice(连接设备)

    • 作用:专为与外部物理设备(如 Bluetooth LE 设备、USB 外设、Wi-Fi P2P 设备、车载系统等)保持实时互动和数据传输设计。
    • 注意:如果服务只是与远程 Web API/服务器通信,不能使用此类型,否则应用在上架 Google Play 审核时会被拒绝。
  2. dataSync(数据同步)

    • 作用:用于应用与远程服务器之间的上传、下载、数据备份或网络同步。
    • 注意:Android 14 引入了超时限制。如果是可以延迟或断点续传的后台任务,官方强烈建议使用 WorkManagerJobScheduler,而不是前台服务。
  3. mediaProcessing(媒体处理)

    • 作用:专门用于 CPU/GPU 密集型的本地媒体文件转换或编辑(例如将 MP4 压缩为 GIF、视频编码、音频滤镜处理)。
    • 注意:仅适用于媒体处理本身。如果是从网络下载媒体文件,应该使用 dataSync;如果是播放音视频,则应使用 mediaPlayback

android15 全面屏模式

在 Android 15(API Level 35)中,全面屏模式(Edge-to-Edge)被强制默认开启。这意味着应用内容默认扩展到状态栏和导航栏下方绘制,开发者无法像旧版本那样简单地选择“关闭”该体验。

核心变化与区别
维度Android 14 及以前Android 15(Target API 35)
默认状态需手动调用 enableEdgeToEdge()setDecorFitsSystemWindows(false) 开启强制默认开启(针对 Target API 35 的应用)
系统栏颜色允许自定义状态栏/导航栏的半透明或不透明背景色状态栏与导航栏背景强制透明或半透明,无法设为完全不透明实体颜色
刘海屏适配可选避开遮挡强制窗口填充屏幕异形/凹槽区域(Dislay Cutout)
禁用选项使用默认外观即避开 Edge-to-Edge仅提供极少数临时避让属性(如 windowOptOutEdgeToEdgeEnforcement),且在未来版本会被完全移除
带来的影响与适配方法

由于应用视图会延伸至状态栏(顶部)和导航栏(底部)下方,如果未做 View 边距处理,UI 元素(如顶部按钮、底部 TabBar 或输入框)可能会被系统遮挡,导致点击失效或文本重叠。

1. 传统 View 布局(XML / Activity)

必须通过 WindowInsets 为被遮挡的 View 添加正确的 padding 或 margin:

Kotlin

ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets ->
    val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
    // 动态调整 View 的内边距,确保内容不被顶部状态栏和底部导航栏覆盖
    view.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
    insets
}
2. Jetpack Compose 布局

Compose 原生提供了完善的 Inset 支持,优先使用 Scaffold 或对应的 Modifier:

  • 使用 Scaffold(自动处理系统边距)

    Kotlin

    Scaffold { innerPadding ->
        Box(modifier = Modifier.padding(innerPadding)) {
            // 内容区域自动避开状态栏和导航栏
        }
    }
    
  • 手动处理 Safe Area

    Kotlin

    Box(modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars)) {
        // 保持针对系统栏的安全边距
    }