6.0以上動態權限
將系統權限區分為正常權限和危險權限。開發者在使用到危險權限相關的功能時,不僅需要在Manifest文件中配置,還需要在代碼中動態獲取權限
需要注意的幾個權限
- 6.0以上需要檢測懸浮窗權限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
/**
* 是否有懸浮窗權限
*/
static boolean isHasOverlaysPermission(Context context) {
if (isOverMarshmallow()) {
return Settings.canDrawOverlays(context);
}
return true;
}
/**
* 用于6.0懸浮窗權限請求
*/
// 跳轉到允許安裝未知來源設置頁面
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + getActivity().getPackageName()));
startActivityForResult(intent, getArguments().getInt(REQUEST_CODE));
- 8.0以上需要檢測以下權限
//未知應用安裝權限
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
/**
* 是否有安裝權限
*/
static boolean isHasInstallPermission(Context context) {
if (isOverOreo()) {
return context.getPackageManager().canRequestPackageInstalls();
}
return true;
}
/**
* 用于8.0安裝第三方應用權限請求
*/
// 跳轉到懸浮窗設置頁面
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getActivity().getPackageName()));
startActivityForResult(intent, getArguments().getInt(REQUEST_CODE));
//允許您的應用通過編程方式接聽呼入電話。要在您的應用中處理呼入電話,您可以使用 acceptRingingCall() 函數。
ANSWER_PHONE_CALLS
//允許您的應用讀取設備中存儲的電話號碼。
READ_PHONE_NUMBERS
代碼
/**
* justin
* 危險權限請求類
*/
class JPermissions {
private var mActivity:Activity
private var mPermissions:MutableList<String> = ArrayList()
private var mRequestAgain: Boolean = false
private constructor(activity:Activity){
mActivity = activity
}
companion object {
/**
* 設置請求的對象
*/
fun with(activity: Activity): JPermissions {
return JPermissions(activity)
}
/**
* 檢查某些權限是否全部授予了
*
* @param context 上下文對象
* @param permissions 需要請求的權限組
*/
fun isHasPermission(context: Context, vararg permissions: String): Boolean {
val failPermissions = PermissionUtils.getDeniedPermissions(context, Arrays.asList(*permissions))
return failPermissions == null || failPermissions.isEmpty()
}
/**
* 跳轉到應用權限設置頁面
*
* @param context 上下文對象
*/
fun gotoPermissionSettings(context: Context) {
PermissionSettingPage.start(context, false)
}
/**
* 跳轉到應用權限設置頁面
*
* @param context 上下文對象
* @param newTask 是否使用新的任務棧啟動
*/
fun gotoPermissionSettings(context: Context, newTask: Boolean) {
PermissionSettingPage.start(context, newTask)
}
}
/**
* 設置需要請求的權限
*/
fun permissions(vararg permissions: String):JPermissions{
mPermissions.addAll(permissions)
return this
}
/**
* 被拒絕后繼續申請,直到授權或者永久拒絕
*/
fun requestAgain(): JPermissions {
mRequestAgain = true
return this
}
/**
* 請求權限
*/
fun requestPermissions(permissionResult:OnPermissionsResult) {
// 如果沒有指定請求的權限,就使用清單注冊的權限進行請求
if (mPermissions.isEmpty()) mPermissions = PermissionUtils.getManifestPermissions(mActivity)
if (mPermissions.isEmpty()) throw IllegalArgumentException("The requested permission cannot be empty")
if (mActivity == null) throw IllegalArgumentException("The activity is empty")
if (permissionResult == null) throw IllegalArgumentException("The permission request callback interface must be implemented")
val checkTargetSdkVersion = PermissionUtils.checkTargetSdkVersion(mActivity,mPermissions)
if (!checkTargetSdkVersion) return
val deniedPermissions = PermissionUtils.getDeniedPermissions(mActivity, mPermissions)
//權限已經全部授予
if (deniedPermissions.isEmpty()){
permissionResult.agreePermission(mPermissions,true)
}else{
// 檢測權限有沒有在清單文件中注冊
PermissionUtils.checkPermissionsIsInManifest(mActivity, mPermissions)
// 申請沒有授予過的權限
PermissionFragment.newInstance(ArrayList(mPermissions), mRequestAgain).prepareRequest(mActivity, permissionResult)
}
}
}
/**
* author:justin
* time:2019/04/22
* desc:權限請求處理類
*/
class PermissionFragment : Fragment, Runnable {
constructor() : super()
companion object {
private val PERMISSIONS: String = "permissions" // 請求的權限
private val REQUEST_CODE: String = "request_code" // 請求碼(自動生成)
private val REQUEST_AGAIN: String = "request_again" // 是否不斷請求
private val sContainer: SparseArray<OnPermissionsResult> = SparseArray()
fun newInstance(permissions: ArrayList<String>, requestAgain: Boolean): PermissionFragment {
val fragment = PermissionFragment()
val bundle = Bundle()
var requestCode: Int
// 請求碼隨機生成,避免隨機產生之前的請求碼,必須進行循環判斷
do {
// requestCode = new Random().nextInt(65535); // Studio編譯的APK請求碼必須小于65536
requestCode = Random().nextInt(255) // Eclipse編譯的APK請求碼必須小于256
} while (sContainer.get(requestCode) != null)
bundle.putInt(REQUEST_CODE, requestCode)
bundle.putStringArrayList(PERMISSIONS, permissions)
bundle.putBoolean(REQUEST_AGAIN, requestAgain)
fragment.arguments = bundle
return fragment
}
}
/**
* 用于6.0懸浮窗 8.0安裝第三方應用權限請求
*/
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val permissions = arguments!!.getStringArrayList(PERMISSIONS)
if (permissions == null || permissions.isEmpty()) return
if (permissions.contains(Manifest.permission.SYSTEM_ALERT_WINDOW) && !PermissionUtils.isHasOverlaysPermission(getActivity())
||permissions.contains(Manifest.permission.REQUEST_INSTALL_PACKAGES) && !PermissionUtils.isHasInstallPermission(getActivity())) {
if (permissions.contains(Manifest.permission.SYSTEM_ALERT_WINDOW) && !PermissionUtils.isHasOverlaysPermission(getActivity())) {
// 跳轉到懸浮窗設置頁面
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + activity!!.getPackageName()))
startActivityForResult(intent, arguments!!.getInt(REQUEST_CODE))
}
if (permissions.contains(Manifest.permission.REQUEST_INSTALL_PACKAGES) && !PermissionUtils.isHasInstallPermission(getActivity())) {
// 跳轉到允許安裝未知來源設置頁面
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:" + activity!!.getPackageName()))
startActivityForResult(intent, arguments!!.getInt(REQUEST_CODE))
}
return
}
requestPermission()
}
/**
* 準備請求
*/
fun prepareRequest(activity: Activity, call: OnPermissionsResult) {
// 將當前的請求碼和對象添加到集合中
sContainer.put(arguments!!.getInt(REQUEST_CODE), call)
activity.fragmentManager.beginTransaction().add(this, activity.javaClass.name).commit()
}
/**
* 請求權限
*/
fun requestPermission() {
if (PermissionUtils.isOverMarshmallow()) {
val permissions = arguments!!.getStringArrayList(PERMISSIONS)
requestPermissions(permissions!!.toTypedArray(), arguments!!.getInt(REQUEST_CODE))
}
}
/**
* 權限請求結果回調(除過6.0懸浮窗 8.0安裝第三方應用回調)
*/
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
val onPermissionsResult = sContainer.get(requestCode)
if (onPermissionsResult == null) return
for (i in permissions.indices) {
// 重新檢查懸浮窗權限
if (Permission.SYSTEM_ALERT_WINDOW == permissions[i]) {
if (PermissionUtils.isHasOverlaysPermission(activity)) {
grantResults[i] = PackageManager.PERMISSION_GRANTED
} else {
grantResults[i] = PackageManager.PERMISSION_DENIED
}
}
// 重新檢查安裝權限
if (Permission.REQUEST_INSTALL_PACKAGES == permissions[i]) {
if (PermissionUtils.isHasInstallPermission(activity)) {
grantResults[i] = PackageManager.PERMISSION_GRANTED
} else {
grantResults[i] = PackageManager.PERMISSION_DENIED
}
}
// 重新檢查8.0的兩個新權限
if (permissions[i] == Permission.ANSWER_PHONE_CALLS || permissions[i] == Permission.READ_PHONE_NUMBERS) {
// 檢查當前的安卓版本是否符合要求
if (!PermissionUtils.isOverOreo()) {
grantResults[i] = PackageManager.PERMISSION_GRANTED
}
}
}
val deniedPermissions = PermissionUtils.getDeniedPermissions(activity as Context, permissions.asList())
if (deniedPermissions.isEmpty()) {
onPermissionsResult.agreePermission(permissions.toList(), true)
} else {
// 檢查是否開啟了繼續申請模式,如果是則檢查沒有授予的權限是否還能繼續申請
if (arguments!!.getBoolean(REQUEST_AGAIN) && PermissionUtils.isRequestDeniedPermission(activity as Activity, deniedPermissions)) {
// 如果有的話就繼續申請權限,直到用戶授權或者永久拒絕
requestPermission()
return
}
// 代表申請的權限中有不同意授予的,如果有某個權限被永久拒絕就返回true給開發人員,讓開發者引導用戶去設置界面開啟權限
onPermissionsResult.disagreePermission(deniedPermissions, PermissionUtils.checkMorePermissionPermanentDenied(activity as Activity, deniedPermissions))
// 證明還有一部分權限被成功授予,回調成功接口
if (permissions.size>deniedPermissions.size){
val succeePermissions = permissions.toMutableList()
val succeed = succeePermissions.removeAll(deniedPermissions)
if (succeed) {
if (!succeePermissions.isEmpty()) {
onPermissionsResult.agreePermission(succeePermissions, false)
}
}
}
}
// 權限回調結束后要刪除集合中的對象,避免重復請求
sContainer.remove(requestCode)
fragmentManager!!.beginTransaction().remove(this).commit()
}
private var isBackCall: Boolean = false // 是否已經回調了,避免安裝權限和懸浮窗同時請求導致的重復回調
/**
* 6.0懸浮窗 8.0安裝第三方應用回調
* 需要注意第三個Intent參數,kotlin的NULL檢查機制,導致不寫?一直回調不成功(坑)
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
// super.onActivityResult(requestCode, resultCode, data);
if (!isBackCall && requestCode == arguments!!.getInt(REQUEST_CODE)) {
isBackCall = true
// 需要延遲執行,不然有些華為機型授權了但是獲取不到權限
Handler(Looper.getMainLooper()).postDelayed(this, 500)
}
}
/**
* [Runnable.run]
*/
override fun run() {
// 請求其他危險權限
requestPermission()
}
}
PermissionFragment通過名字也能看出,這里采用了Fragment來處理請求回調,Fragment的妙用有很多,比如解決自定義view綁定生命周期的問題(百度地圖還需要用戶自己去綁定生命周期,不友好啊...)
/**
* 權限請求工具類
*/
class PermissionUtils {
companion object {
/**
* 獲取清單文件中的權限
*/
fun getManifestPermissions(context: Context): MutableList<String> {
return context.packageManager.getPackageInfo(context.packageName,
PackageManager.GET_PERMISSIONS).requestedPermissions.toMutableList()
}
/**
* 檢查targetSdkVersion是否符合要求
*
* @param context 上下文對象
* @param requestPermissions 請求的權限組
*/
fun checkTargetSdkVersion(context: Context, requestPermissions: List<String>):Boolean {
if (requestPermissions.contains(Manifest.permission.REQUEST_INSTALL_PACKAGES)
|| requestPermissions.contains(Manifest.permission.ANSWER_PHONE_CALLS)
|| requestPermissions.contains(Manifest.permission.READ_PHONE_NUMBERS)) {
// 必須設置 targetSdkVersion >= 26 才能正常檢測權限
if (context.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.O) {
return true
}
}
if (context.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.M) {
return true
}
return false
}
/**
* 獲取未授權權限
*/
fun getDeniedPermissions(context: Context, requestPermissions: List<String>):MutableList<String>{
var deniedList:MutableList<String> = ArrayList()
for (permission in requestPermissions) {
//檢測6.0懸浮窗權限
if (TextUtils.equals(permission,Manifest.permission.SYSTEM_ALERT_WINDOW)){
if (!isHasOverlaysPermission(context)) {
deniedList.add(permission)
continue
}
}
//檢測8.0以上安裝未知來源應用權限
if (TextUtils.equals(permission,Manifest.permission.REQUEST_INSTALL_PACKAGES)){
if (!isHasInstallPermission(context)){
deniedList.add(permission)
continue
}
}
//檢測8.0的兩個新權限
if (TextUtils.equals(permission,Manifest.permission.ANSWER_PHONE_CALLS)
||TextUtils.equals(permission,Manifest.permission.READ_PHONE_NUMBERS)){
if (!isOverOreo()){
continue
}
}
// 把沒有授予過的權限加入到集合中
if (ActivityCompat.checkSelfPermission(context,permission) == PackageManager.PERMISSION_DENIED) {
deniedList.add(permission)
}
}
return deniedList
}
/**
* 檢測6.0懸浮窗權限
*/
fun isHasOverlaysPermission(context: Context): Boolean {
if (isOverMarshmallow()){
return Settings.canDrawOverlays(context)
}
return true
}
/**
* 檢測8.0以上安裝未知來源應用權限
*/
fun isHasInstallPermission(context: Context): Boolean {
if (isOverOreo()){
return context!!.packageManager.canRequestPackageInstalls()
}
return true
}
/**
* 是否是6.0以上版本
*/
fun isOverMarshmallow(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
}
/**
* 是否是8.0以上版本
*/
fun isOverOreo(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
}
/**
* 檢測權限是否在清單文件中注冊
*/
fun checkPermissionsIsInManifest(context: Context,requestPermissions: List<String>){
val manifestPermissions = getManifestPermissions(context)
if (manifestPermissions != null&&!manifestPermissions.isEmpty()){
for (permission in requestPermissions) {
if (!manifestPermissions.contains(permission)){
throw RuntimeException(permission + ": Permissions are not registered in the manifest file")
}
}
}else{
throw RuntimeException("No permissions are registered in the manifest file")
}
}
/**
* 在權限組中檢查是否有某個權限是否被永久拒絕
*
* @param activity Activity對象
* @param permissions 請求的權限
*/
fun checkMorePermissionPermanentDenied(activity: Activity, permissions: List<String>): Boolean {
for (permission in permissions) {
// 安裝權限和浮窗權限不算,本身申請方式和危險權限申請方式不同,因為沒有永久拒絕的選項,所以這里返回false
if (permission == Permission.REQUEST_INSTALL_PACKAGES || permission == Permission.SYSTEM_ALERT_WINDOW) {
continue
}
if (checkSinglePermissionPermanentDenied(activity, permission)) {
return true
}
}
return false
}
/**
* 檢查某個權限是否被永久拒絕
*
* @param activity Activity對象
* @param permission 請求的權限
*/
fun checkSinglePermissionPermanentDenied(activity: Activity, permission: String): Boolean {
// // 安裝權限和浮窗權限不算,本身申請方式和危險權限申請方式不同,因為沒有永久拒絕的選項,所以這里返回false
// if (permission.equals(Permission.REQUEST_INSTALL_PACKAGES) || permission.equals(Permission.SYSTEM_ALERT_WINDOW)) {
// return false;
// }
// 檢測8.0的兩個新權限
if (permission == Permission.ANSWER_PHONE_CALLS || permission == Permission.READ_PHONE_NUMBERS) {
// 檢查當前的安卓版本是否符合要求
if (!isOverOreo()) {
return false
}
}
if (PermissionUtils.isOverMarshmallow()) {
if (activity.checkSelfPermission(permission) == PackageManager.PERMISSION_DENIED && !activity.shouldShowRequestPermissionRationale(permission)) {
return true
}
}
return false
}
/**
* 是否還能繼續申請沒有授予的權限
*
* @param activity Activity對象
* @param failPermissions 失敗的權限
*/
fun isRequestDeniedPermission(activity: Activity, failPermissions: List<String>): Boolean {
for (permission in failPermissions) {
// 安裝權限和浮窗權限不算,本身申請方式和危險權限申請方式不同,因為沒有永久拒絕的選項,所以這里返回false
if (permission == Permission.REQUEST_INSTALL_PACKAGES || permission == Permission.SYSTEM_ALERT_WINDOW) {
continue
}
// 檢查是否還有權限還能繼續申請的(這里指沒有被授予的權限但是也沒有被永久拒絕的)
if (!checkSinglePermissionPermanentDenied(activity, permission)) {
return true
}
}
return false
}
/**
* 獲取已授予的權限
*
* @param permissions 需要請求的權限組
* @param grantResults 允許結果組
*/
fun getSucceedPermissions(permissions: Array<String>, grantResults: IntArray): List<String> {
val succeedPermissions = java.util.ArrayList<String>()
for (i in grantResults.indices) {
// 把授予過的權限加入到集合中,-1表示沒有授予,0表示已經授予
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
succeedPermissions.add(permissions[i])
}
}
return succeedPermissions
}
}
}
/**
* author:justin
* time:2019/04/24
* desc: 權限設置頁(兼容大部分國產手機)
*/
internal object PermissionSettingPage {
/**
* 手機制造商
*/
private val MARK = Build.MANUFACTURER.toLowerCase()
/**
* 跳轉到應用權限設置頁面
*
* @param context 上下文對象
* @param newTask 是否使用新的任務棧啟動
*/
fun start(context: Context, newTask: Boolean) {
var intent: Intent? = null
if (MARK.contains("huawei")) {
intent = huawei(context)
} else if (MARK.contains("xiaomi")) {
intent = xiaomi(context)
} else if (MARK.contains("oppo")) {
intent = oppo(context)
} else if (MARK.contains("vivo")) {
intent = vivo(context)
} else if (MARK.contains("meizu")) {
intent = meizu(context)
}
if (intent == null || !hasIntent(context, intent)) {
intent = google(context)
}
if (newTask) {
//如果用戶在權限設置頁面改動了權限,個別手機請求權限的activity會被重啟,解決該問題
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
try {
context.startActivity(intent)
} catch (ignored: Exception) {
intent = google(context)
context.startActivity(intent)
}
}
private fun google(context: Context): Intent {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intent.data = Uri.fromParts("package", context.packageName, null)
return intent
}
private fun huawei(context: Context): Intent {
val intent = Intent()
intent.component = ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity")
if (hasIntent(context, intent)) return intent
intent.component = ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.addviewmonitor.AddViewMonitorActivity")
if (hasIntent(context, intent)) return intent
intent.component = ComponentName("com.huawei.systemmanager", "com.huawei.notificationmanager.ui.NotificationManagmentActivity")
return intent
}
private fun xiaomi(context: Context): Intent {
val intent = Intent("miui.intent.action.APP_PERM_EDITOR")
intent.putExtra("extra_pkgname", context.packageName)
if (hasIntent(context, intent)) return intent
intent.setPackage("com.miui.securitycenter")
if (hasIntent(context, intent)) return intent
intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity")
if (hasIntent(context, intent)) return intent
intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity")
return intent
}
private fun oppo(context: Context): Intent {
val intent = Intent()
intent.putExtra("packageName", context.packageName)
intent.setClassName("com.color.safecenter", "com.color.safecenter.permission.floatwindow.FloatWindowListActivity")
if (hasIntent(context, intent)) return intent
intent.setClassName("com.coloros.safecenter", "com.coloros.safecenter.sysfloatwindow.FloatWindowListActivity")
if (hasIntent(context, intent)) return intent
intent.setClassName("com.oppo.safe", "com.oppo.safe.permission.PermissionAppListActivity")
return intent
}
private fun vivo(context: Context): Intent {
val intent = Intent()
intent.setClassName("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.FloatWindowManager")
intent.putExtra("packagename", context.packageName)
if (hasIntent(context, intent)) return intent
intent.component = ComponentName("com.iqoo.secure", "com.iqoo.secure.safeguard.SoftPermissionDetailActivity")
return intent
}
private fun meizu(context: Context): Intent {
val intent = Intent("com.meizu.safe.security.SHOW_APPSEC")
intent.putExtra("packageName", context.packageName)
intent.component = ComponentName("com.meizu.safe", "com.meizu.safe.security.AppSecActivity")
return intent
}
private fun hasIntent(context: Context, intent: Intent): Boolean {
return !context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).isEmpty()
}
}
/**
* 權限請求結果回調接口
*/
interface OnPermissionsResult {
fun agreePermission(granted:List<String> , agreeAll:Boolean )
fun disagreePermission(disagree:List<String> , never:Boolean )
}
使用
JPermissions.with(this)
.permissions(Manifest.permission.CAMERA,Manifest.permission.CALL_PHONE)//如果不設置,自動獲取manifest文件下權限
.requestAgain()//拒絕后再次請求,直到用戶永久拒絕
.requestPermissions(object :OnPermissionsResult{
override fun agreePermission(granted: List<String>, agreeAll: Boolean) {
if (agreeAll){
//獲取所有權限成功
Toast.makeText(this@MainActivity,"全部請求成功",Toast.LENGTH_LONG).show()
}else{
//獲取部分權限成功
Toast.makeText(this@MainActivity,granted.toString()+"部分請求成功",Toast.LENGTH_LONG).show()
}
}
override fun disagreePermission(disagree: List<String>, never: Boolean) {
if (never){
Toast.makeText(this@MainActivity,disagree.toString()+"永久拒絕",Toast.LENGTH_LONG).show()
//被永久拒絕就跳轉到應用權限系統設置頁面
JPermissions.gotoPermissionSettings(this@MainActivity)
}else{
//獲取權限失敗
Toast.makeText(this@MainActivity,disagree.toString()+"獲取失敗",Toast.LENGTH_LONG).show()
}
}
})