一. 前言
最近在搞这一块的东西, 要求用户给我们app以位置与通知的权限. 尤其是位置, 这样我们可以根据用户位置, 推荐用户旁边的店铺之类的. 既然要推荐, 那自然用户不可能一直开着我们app, 所以我们请求用户给予我们app在后台也能请求location的权限, 以及得到了位置后, 我们要能发通知给用户, 告诉用户旁边有好店好物. 总之, 我们就是要请求用户给我们:
- precise (也叫fine)的location权限
- background的location权限
- notification权限
当我开始动手做时, 我就发现了麻烦的地方了, 也就是每个Android版本的权限不一样, 这给我的代码编写带来了不少麻烦. 我一一克服后, 也就在此分享下, 希望能帮到有需要的同学.
二. 各个Android版本的Loation权限的变化
《android定位权限适配看这篇就够了》其实讲得不错, 讲了各个版本的情况, 但这文章目前看来有几个缺陷:
- 成文较早, 只覆盖到Android 11. 而Android 12上location权限有又变化, 此文因此未能覆盖到Android 12+的情况
- 代码较少. 当涉及到如何读取是否有权限 , 以及如何申请权限, 都没有涉及到.
所以我要针对这些问题加以一定的修正, 并做一些补强, 也就是:
- 对于已经有部分权限, 再申请全权限时, 会发生什么? 我也想讲清楚. 这样就能覆盖更多的测试用例/使用场景.
- 使用更多对比型的图片, 这样让大家更容易理解各个版本间的区别.
2.1 各个版本的变化
备注: 下方出现的launcher
, 是指ActivityResultLauncher, 即这样的:
kotlin
private lateinit var launcher : ActivityResultLauncher<Array<String>>
launcher = registerForActivityResult(RequestMultiplePermissions()) {map: Map<String, Boolean> ->
// .....
}
-
Android 9: 位置相关的权限只有一个就是location权限
-
Android 10: 新加了一个background_location权限. 分别表示app在前台与后台时能允许定位的权限
- Android 10还有一个特点, 就是它能允许app请求background_location权限, 即
launcher.launch(fine_location, bg_location)
- Android 10还有一个特点, 就是它能允许app请求background_location权限, 即
-
Android 11: app不能同时请求location与bg_location权限了. 这样的请求会啥都不出现, 自然launcher也不会有任何结果
- Android 11只能在请求Location成功后, 再请求bg_location权限. 而同时声明location + bg_location, 不会有任何反应的.
-
Android 12 : 普通的location权限再次被细分成了precise(也叫fine)与approximate(也叫coarse)权限. 这样一来, 我们的location相关的权限就有三种了: precise, coarse, bg.
2.2 图示
其实打开任一个app的app info, 即它们的setting页, 然后再找到location相关权限的那一页, 就能看出各个版本的区别:
说明
Android10+上的:
- "Allow all the time": 其实就是说有了precise/coarse location权限, 并再给予bg_location权限:
- "Allow while using": 其实就是说 只有precise/coarse location权限, 并没有bg_location权限
三. 如何查询Location权限?
我们查询一个权限, 可以用下面的ext方法:
kotlin
fun Context.isPermissionGranted(permission: String): Boolean =
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
但每个Android版本都不一样啊, 所以我就有了两个大疑问:
1). 既然fine_location在Android 12+才出现; 那我们在Android 11-的设置上运行 ctx.isPermissionGranted(fine_loc)能拿到正确的值吗?
2). 既然bg_location是Android10之后才有的权限; 那在Android 9-的设备上运行 ctx.isPermissionGranted(bg_loc)是否会有结果呢? 会有什么样的结果 呢?
多说无益, 在各个版本上运行下代码来看下结果:
kotlin
val isFineLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)
val isCoarseLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION)
val isBackgroundLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
println("fine = $isFineLocationGranted, coarse = $isCoarseLocationGranted, bg = $isBackgroundLocationGranted")
图示
总结成一个图那就是:
这里要注意几点哦:
- 因为Android 9-并没有限制后台请求定位的权限. 所以其实只要打开了location权限, 就相当于后台也能请求定位了. 这时我们预期的结果应该是
fine == coarse == bg
.- 但结果不是这样的. 结果是
fine == corase == true, bg = false
, 或是fine == coarse == false, bg = false
.
- 但结果不是这样的. 结果是
总之就是, 在Android 9-上, 虽然可能已经有了bg定位的能力了, 但去请求BACKGROUND_LOCATION
权限给予了否, 结果总是false. 这是个坑, 请注意哦!
- 另外就是在Android 11-的设备上, fine权限始终和coarse权限是一样的. 都能真实反应用户是否给予了普通定位权限, 可以放心用.
代码实践
kotlin
fun Context.isPreciseLocationGranted() =
this.isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)
// Android 12-的普通定位权限, 并没有fine与coarse之分的; coarse等同于fine权限
fun Context.isCoarseLocationGranted() : Boolean {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return this.isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)
} else {
return this.isPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION)
}
}
fun Context.isBackgroundLocationGranted() : Boolean{
// Android 9-的bg_location权限, 在请求时总是返回false; 所以这里要用普通定位来得到其真实的值
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return this.isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)
} else {
return this.isPermissionGranted(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
}
}
四, 如何申请location权限
申请权限并不难, 我们可以用Androidx的ActivityResult的新API来申请权限
kotlin
// 声明与注册callback
private lateinit var launcher : ActivityResultLauncher<Array<String>>
launcher = registerForActivityResult(RequestMultiplePermissions()) {map: Map<String, Boolean> ->
// .....
}
// 申请权限
launcher.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
))
//或是申请单一权限:
launcher.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
))
但麻烦其实是隐藏在不同版本的不同行为中的. 而且没有权限时去申请, 以及有了部分权限再去申请也不一样.
4.1 无任何权限时去请求
这个用文字说不太明显了, 来看下图吧, 对比性一眼就看出来了.
(图太大了, 只好分成几个图, sorry)
说明下:
-
图中的"三权限"就是指全权限, 也就是: ACCESS_FINE_LOCATION + ACCESS_COARSE_LOCATION + ACCESS_BACKGROUND_LOCATION
-
Android 11+就不允许用户同时请求普通定位与bg定位权限了. 所以下面的代码, 在Android11+上不会出现权限申请dialog, 是啥都不会发生.
kotlin
launcher.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
))
- 但是在Android 11+的机器上, 若已经有了普通定位权限, 再申请bg是可以的. 这个具体的结果请见下面.
4.2 有部分权限时再去请求
仍是用图说话
这里的区别其实在上面讲过, 就已经很容易理解了.
多说一句那就是在Android 11+中, 若真的想一个按钮点击就得到所有location权限, 可以考虑这样:
kotlin
private lateinit var launcher2 : ActivityResultLauncher<Array<String>>
// callback中判断是否是是普通权限已经到手了.
launcher2 = registerForActivityResult(RequestMultiplePermissions()) {map: Map<String, Boolean> ->
val isForegroundLocations = map.keys == setOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION,)
val isForegroundLocationsGranted = map.values.contains(true) //任一权限有了, 就可以去请求bg权限了
if(isForegroundLocations && isForegroundLocationsGranted) {
launcher2.launch(arrayOf(Manifest.permission.ACCESS_BACKGROUND_LOCATION))
}
}
// 点击就申请权限
vb.btn.text = "And11+上想一个按钮就请求全权限?"
vb.btn.setOnClickListener {
launcher2.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
))
}
4.3 做下封装
我们app的需求是:
- 没有precise权限就申请precise的location权限
- 若有precise权限那就是打开OS Setting页, 让用户来选择"Allow all the time"
其实实现这需求有点小麻烦, 主要是各个版本的Android上行为不一样.
特别是Android10可以允许请求后台定位权限, 但Android11+又不让.
于是我们小小封装一下:
kotlin
fun computeNecessaryLocationPermissions(): Array<String> {
// 1). Android 11+ (R+), background_location 权限不能被直接请求
// 2). Android 12+, 最好是连着Fine & Coarse一起请求. (这一点也不影响Android 11系统)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
return arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)
}
// Android 10(Q), background_location permissio是可以被app请求的
else if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
return arrayOf(ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION)
}
// Android 9-, 没有background_location权限, 直接申请 Fine_location即可
else {
return arrayOf(ACCESS_FINE_LOCATION)
}
}
然后我们的请求申请权限的按钮点击后是这样的:
kotlin
val isBackgroundLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
val isCoarseLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION)
val isFineLocationGranted = isPermissionGranted(Manifest.permission.ACCESS_FINE_LOCATION)
if(isFineLocationGranted && !isBackgroundLocationGranted) {
context.openOsSetting()
} else {
launcher.launch(computeNecessaryLocationPermissions())
}
fun Context.openOsSetting() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", packageName, null)
startActivity(intent)
}
fun Context.isPermissionGranted(permission: String): Boolean =
ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
五. 各个Android版本的Notification权限的变化
上面讲完了Location权限. 现在轮到了Notification权限. 其实通知权限也有类似问题, 只不过没这么复杂.
- Android 12-是没有
POST_NOTIFICATION
权限的.- 新安装的app, 其通知权限默认是打开的
- Android 13+才新加了
POST_NOTIFICATION
权限的- 新安装的app, 其通知权限默认是关闭的
既然
Manifest.permission.POST_NOTIFICATIONS
是 Android 13 (api level 33, TIRAMISU)里才有的新权限, 那在旧版本上查询与申请这个POST_NOTIFICATION
权限, 会能用吗?
5.1 查询
经过我的实践, 在任意平台, 都可以查询这个结果, 而且结果都是正确的.
- 即你开了通知权限, 查询到的结果就是true (给了权限) ;
- 若你去设置页面关了通知权限, 那查询到的结果就是false
kotlin
private lateinit var singlePermissionLauncher : ActivityResultLauncher<String>
singlePermissionLauncher = registerForActivityResult(RequestPermission()) {
// ....
}
//申请权限
singlePermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
5.2 申请权限
这里才是个坑. 尽管如上所说, 你在全平台, 查询POST_NOTIFICATIONS权限都没问题, 而且结果都是正确的.
但你想在Android 12-上申请POST_NOTIFICATIONS权限, 那就啥反应都没有. 而且你的launcher的callback也不会被调用!!
在Android 12-要想打开通知权限, 就只能去OS Setting页了.
总结一下, 申请通知权限的代码就是:
kotlin
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
singlePermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
this.openNotificationOsSetting()
}
其中的openNotificationOsSetting函数是个扩展函数, 内容是:
kotlin
fun Activity.openNotificationOsSetting() {
try {
val aIntent = Intent()
aIntent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS
//8.0及以后版本使用这两个extra. >=API 26
aIntent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
aIntent.putExtra(Settings.EXTRA_CHANNEL_ID, applicationInfo.uid)
//5.0-7.1 使用这两个extra. <= API 25, >=API 21
aIntent.putExtra("app_package", packageName)
aIntent.putExtra("app_uid", applicationInfo.uid)
startActivity(aIntent)
} catch (ex: Exception) {
//有些系统会crash说找不到activity来接收这个intent, 所以要try-catch一下
}
}
六. 总结
其实经过上面的描述与各种图示, 我们已经知道location与notification权限在各个平台的不一致性给我们带来了不少麻烦. 我在实践过程中一一做了封装, 方便了查询权限与申请权限. 希望本文也能帮到需要请求权限的你.