最近在做毕设,里面用到了关于权限申请的东东。我们知道Android系统在API23(6.0 Marshallow)之后对于permission level为dangerous的权限做了进一层的保护,除了需要在Manifest中注册,还需要在对这些权限进行动态权限申请,必须用户手动授权才能获取这些权限的使用。
以下就是level被标记为dangerous的permission:
<!-- CALENDAR 日历组 -->
<uses-permission android:name="android.permission.READ_CALENDAR" />
<uses-permission android:name="android.permission.WRITE_CALENDAR" />
<!-- CAMERA 相机拍照组 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- CONTACTS 联系人组 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!-- LOCATION 定位组 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- MICROPHONE 麦克风组 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<!-- PHONE 组 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
<uses-permission android:name="android.permission.USE_SIP" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<!-- SENSORS 传感器组 -->
<uses-permission android:name="android.permission.BODY_SENSORS" />
<!-- SMS 组 -->
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH" />
<uses-permission android:name="android.permission.RECEIVE_MMS" />
<!-- STORAGE 存储组 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
相信各位大佬们都已经对上面这些都已经很熟悉了,但是鉴于每次动态权限申请时都要写一大堆重复的代码,从前我写项目时也只是那么几个权限,觉得并不需要封装一个工具类。但是这次毕设还是想把项目写得规范一点,故接下来是这篇博客的主要内容——如何编写一个代码复用性高的工具类来替代每次动态权限申请的重复代码。
这里我用了链式调用的方式来使用PermissionUtils,主要还是谈一下最主要的功能代码以及踩过的一些坑。
先来看看请求权限时的代码:
fun request(callback: PermissionCallback) {
this.mCallback = callback
if (Build.VERSION.SDK_INT >= 23) {
var isAllGranted = true
mPermissions.forEach {
if (ContextCompat.checkSelfPermission(mActivity, it) == PackageManager.PERMISSION_DENIED) {
// 说明此权限可能还没被申请成功
isAllGranted = false
// 说明一下 需要判断一下"不再提示"的状态
// 第一次申请 shouldShowRationale = false
// 非首次申请时 会出现"不在提示"的UI shouldShowRationale = true
// 若之后选择了"不再提示" 则不会再出现该UI shouldShowRationale = false
if (!ActivityCompat.shouldShowRequestPermissionRationale(mActivity, it)) {
if (mPermissionStatusList[it] == null) {
// 证明在该状态列表中 此权限的状态还未被初始化 这时置该权限位为denied
mPermissionStatusList[it] = STATE.DENIED
} else {
// 此时说明已经有状态了 证明非第一次 那么此时就把该标记为记为always_denied
mPermissionStatusList[it] = STATE.ALWAYS_DENIED
}
} else {
// 如果为true 那么说明也是非首次出现 那么就直接置denied就行了
mPermissionStatusList[it] = STATE.DENIED
}
} else {
mPermissionStatusList[it] = STATE.GRANTED
}
}
if (isAllGranted) {
// mPermissions里所有的权限都申请完成了 直接做操作
Log.e(TAG, "request isAllGranted.")
callback.granted()
return
}
if (mPermissions.isNotEmpty()) {
Log.e(TAG, "ActivityCompat.requestPermissions======")
ActivityCompat.requestPermissions(mActivity, mPermissions, mRequestCode)
}
} else {
// api低于23 不需要动态申请权限 只需要在Manifest中声明即可 因此这里可以直接回调出去做权限认证成功的操作
mCallback?.granted()
}
}
这里主要谈一下这个shouldShowRequestPermissionRationale()的方法。首先这个方法在源码里解释说实话我是看的有点一头雾水的,后来看了一下其他博客,又再打印了一下数据发现其实这个方法其实就是判断在弹出权限申请框时这个UI是否显示:
我在代码的注释里也写得比较清楚,这个UI的出现规则是这样的:
- 首次申请权限时,这个方法返回false(即不需要显示这个UI)
- 非首次申请权限(第一次选择deny或者操作被打断)时,这个方法返回true,那么就会显示这个UI
- 当选择了这个选项,那么这个往后这个方法都会返回false(不显示此UI)
那么这个方法能帮我们干什么呢?我们知道如果当用户选择了不再提示的选项后,下次再申请该权限时这个权限申请框是不会再弹出了,那么我们就需要去引导用户去设置页面手动允许我们需要的权限。所以这里的mPermissionStateList是用来记录每个权限的状态,以便后续我们操作。
我们知道在ActivityCompat.requestPermissions()方法之后,API高于等于23的会走动态权限的方法。最后会回调到activity的onRequestPermissionResult()方法。所以我们的PermissionUtils中也得有相关的方法来帮助我们处理回调结果。
fun onRequestPermissionResult(requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray) {
Log.e(TAG, "onRequestPermissionResult")
if (requestCode == CODE_MULTI) {
onRequestMultiPermissionResult(requestCode, permissions, grantResults)
return
}
if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
mPermissionStatusList[permissions[0]] = STATE.GRANTED
mCallback?.granted()
} else {
// 回调出去
mCallback?.denied()
}
}
private fun onRequestMultiPermissionResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
Log.e(TAG, "onRequestMultiPermissionResult")
var isAllGranted = true
permissions.forEachIndexed { index, permission ->
if (grantResults[index] == PackageManager.PERMISSION_GRANTED) {
mPermissionStatusList[permission] = STATE.GRANTED
} else {
isAllGranted = false
}
}
if (isAllGranted) {
mCallback?.granted()
} else {
mCallback?.denied()
}
}
所以我们在activity收到权限申请的回调后,通过callback回调给需要的页面去做需要的操作。
Talk is cheap, show me the code !
class PermissionUtils private constructor() {
private enum class STATE {
GRANTED, // 同意
DENIED, // 拒绝
ALWAYS_DENIED // 不再提示
}
companion object {
private const val TAG = "PermissionUtils"
@SuppressLint("StaticFieldLeak")
private var mInstance: PermissionUtils? = null
const val CODE_READ_STORAGE = 0x01
const val CODE_WRITE_STORAGE = 0X02
const val CODE_CAMERA = 0x03
const val CODE_LOCATION_FINE = 0X04
const val CODE_LOCATION_COARSE = 0x05
const val CODE_READ_PHONE_STATE = 0x06
const val CODE_MULTI = 0x0f
fun getInstance(): PermissionUtils {
if (mInstance == null) {
synchronized(PermissionUtils::class.java) {
mInstance = PermissionUtils()
}
}
checkNotNull(mInstance) { "instance should not be null" }
return mInstance!!
}
}
private lateinit var mActivity: Activity
private var mRequestCode = -1
private var mCallback: PermissionCallback? = null
private lateinit var mPermissions: Array<out String>
private val mPermissionStatusList = ConcurrentHashMap<String, STATE>()
private var mDialog: AlertDialog? = null
fun destroy() {
if (mDialog != null) {
mDialog?.isShowing ?: mDialog?.dismiss()
mDialog = null
}
if (mInstance != null) {
mInstance = null
}
}
fun with(_object: Any): PermissionUtils {
if (_object is Activity) {
mActivity = _object
return this
}
if (_object is Fragment) {
checkNotNull(_object.activity) { "fragment activity is null, cannot init permissionUtils" }
mActivity = _object.activity!!
return this
}
throw IllegalArgumentException("_object is not fragment or activity, cannot init permissionUtils")
}
fun requestCode(requestCode: Int): PermissionUtils {
require(requestCode >= 0) { "request code should be unless as 0" }
this.mRequestCode = requestCode
return this
}
fun permissions(vararg permissions: String): PermissionUtils {
this.mPermissions = permissions
return this
}
fun request(callback: PermissionCallback) {
this.mCallback = callback
if (Build.VERSION.SDK_INT >= 23) {
var isAllGranted = true
mPermissions.forEach {
if (ContextCompat.checkSelfPermission(mActivity, it) == PackageManager.PERMISSION_DENIED) {
// 说明此权限可能还没被申请成功
isAllGranted = false
// 说明一下 需要判断一下"不再提示"的状态
// 第一次申请 shouldShowRationale = false
// 非首次申请时 会出现"不在提示"的UI shouldShowRationale = true
// 若之后选择了"不再提示" 则不会再出现该UI shouldShowRationale = false
if (!ActivityCompat.shouldShowRequestPermissionRationale(mActivity, it)) {
if (mPermissionStatusList[it] == null) {
// 证明在该状态列表中 此权限的状态还未被初始化 这时置该权限位为denied
mPermissionStatusList[it] = STATE.DENIED
} else {
// 此时说明已经有状态了 证明非第一次 那么此时就把该标记为记为always_denied
mPermissionStatusList[it] = STATE.ALWAYS_DENIED
}
} else {
// 如果为true 那么说明也是非首次出现 那么就直接置denied就行了
mPermissionStatusList[it] = STATE.DENIED
}
} else {
mPermissionStatusList[it] = STATE.GRANTED
}
}
if (isAllGranted) {
// mPermissions里所有的权限都申请完成了 直接做操作
Log.e(TAG, "request isAllGranted.")
callback.granted()
return
}
if (mPermissions.isNotEmpty()) {
Log.e(TAG, "ActivityCompat.requestPermissions======")
ActivityCompat.requestPermissions(mActivity, mPermissions, mRequestCode)
}
} else {
// api低于23 不需要动态申请权限 只需要在Manifest中声明即可 因此这里可以直接回调出去做权限认证成功的操作
mCallback?.granted()
}
}
fun onRequestPermissionResult(requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray) {
Log.e(TAG, "onRequestPermissionResult")
if (requestCode == CODE_MULTI) {
onRequestMultiPermissionResult(requestCode, permissions, grantResults)
return
}
if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
mPermissionStatusList[permissions[0]] = STATE.GRANTED
mCallback?.granted()
} else {
// 回调出去
mCallback?.denied()
}
}
private fun onRequestMultiPermissionResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
Log.e(TAG, "onRequestMultiPermissionResult")
var isAllGranted = true
permissions.forEachIndexed { index, permission ->
if (grantResults[index] == PackageManager.PERMISSION_GRANTED) {
mPermissionStatusList[permission] = STATE.GRANTED
} else {
isAllGranted = false
}
}
if (isAllGranted) {
mCallback?.granted()
} else {
mCallback?.denied()
}
}
fun showDialog() {
mActivity ?: return
if (mDialog == null) {
mDialog = AlertDialog.Builder(mActivity)
.setMessage("有权限设置了不再提示,会影响app使用哦~快去\"设置\"页允许吧~")
.setPositiveButton("前往") { dialog, _ ->
goToSetting()
dialog.dismiss()
}
.setNegativeButton("取消") { dialog, _ ->
dialog.dismiss()
}
.setCancelable(true)
.create()
}
mDialog?.show()
}
private fun goToSetting() {
Log.e(TAG, "goToSetting")
val packageName = mActivity.packageName
val toSetting = Intent()
toSetting.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
val uri = Uri.fromParts("package", packageName, null)
toSetting.data = uri
mActivity.startActivity(toSetting)
}
interface PermissionCallback {
fun granted()
fun denied()
}
}
具体的用法只需要在需要的页面使用即可。
注意!!一定要在activity的onRequestPermissionsResult的方法里再进行调用!!不然可能无法回调到具体页面哦~
坑!!
- onRequestPermissionsResult()方法是Activity里重载才会有效,Fragment中重载是无效的,不然会收不到权限申请框点击后的回调。
- 不要对Fragment的构造方法私有化,否则在跳转Setting页面并更改权限后返回app会crash。这是由于在更改权限后,会使app进行重新的创建,那么savedInstanceState就不会为空了,但是由于其保存的状态变量有限,故如果在前往设置页授权的页面中存在Fragment,会走Fragment的空构造方法来恢复页面。如果设置为private,那么就会报Fragment找不到的crash。