最近在项目上因为6.0运行时权限吃了亏,发现之前对运行时权限的理解不足,决定回炉重造,重新学习一下Android Permission。

进入正题:

Android权限

在Android系统中,权限分为三种:正常权限、危险权限和特殊权限:

  •  正常权限:不会直接给用户隐私权带来风险。如果您的应用在其清单中列出了正常权限,系统将自动授予该权限。 
  •  危险权限:涵盖应用需要涉及用户隐私信息的数据或资源,或者可能对用户存储的数据或其他应用的操作产生影响的区域。例如,能够读取用户的联系人属于危险权限。如果应用声明其需要危险权限,则用户必须明确向应用授予该权限。
  • 特殊权限:主要指 SYSTEM_ALERT_WINDOW 和 WRITE_SETTINGS,从官方文档和实际应用可以知道,这两个权限必须在清单中声明,在需要使用的时候发送用户授权的intent,并在指定的方法中回调授权申请情况。(这里在后面详细分析)

权限组:

  • 当应用请求AndroidManifest中列出的危险权限,而应用目前在权限组中没有任何权限,则系统会向用户显示一个对话框,描述应用要访问的权限组。对话框不描述该组内的具体权限。例如,如果应用请求 READ_CONTACTS 权限,系统对话框只说明该应用需要访问设备的联系信息。如果用户批准,系统将向应用授予其请求的权限。
  • 如果应用请求其清单中列出的危险权限,而应用在同一权限组中已有另一项危险权限,则系统会立即授予该权限,而无需与用户进行任何交互。例如,如果某应用已经请求并且被授予了 READ_CONTACTS 权限,然后它又请求 WRITE_CONTACTS,系统将立即授予该权限。
  • 任何权限都可属于一个权限组,包括正常权限应用定义的权限。但权限组仅当权限危险时才影响用户体验。可以忽略正常权限的权限组。
  • 如果设备运行的是 Android 5.1(API 级别 22)或更低版本,并且应用的 targetSdkVersion 是 22 或更低版本,则系统会在安装时要求用户授予权限

(参考自https://developer.android.com/guide/topics/security/permissions.html#normal-dangerous)

  危险权限和权限组-->https://developer.android.com/guide/topics/security/permissions.html#perm-groups

  正常权限----------->https://developer.android.com/guide/topics/security/normal-permissions.html

  

  所有Android版本的应用都需要在Androidmanifest.xml中声明正常权限和危险权限。当然在不同的版本中声明的影响会有所不同。

声明权限:

  将<uses-permission>作为顶级<manifest>元素的子项加入到Androidmanifest.xml中:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.snazzyapp"> <uses-permission android:name="android.permission.SEND_SMS"/> <application ...>
...
</application> </manifest>

  

  根据系统的不同,对于危险权限有不同的处理方式:

  •  若设备运行的是Android5.1或更低版本,或者应用的targetSDK为22或更低:如果您在清单中列出了危险权限,则用户必须在安装应用时授予此权限;如果他们不授予此权限,系统根本不会安装应用。
  • 若设备运行的是Android6.0或更高版本,或者应用的targetSDK为23或更高:应用必须在清单中列出权限,并且它必须在运行时请求其需要的每项危险权限。用户可以授予或拒绝每项权限,且即使用户拒绝权限请求,应用仍可以继续运行有限的功能。

  那么在6.0下该怎么处理危险权限的申请呢?

6.0下的权限处理

  可以分为三步:

    1.检查权限

    2.请求权限

    3.请求回调处理

    1.检查权限

      为了兼容旧版本,这里使用ContextCompat的API,详细的信息看http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0830/3387.html的“用兼容库使代码兼容旧版”部分

    

// Assume thisActivity is the current activity
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.WRITE_CALENDAR);

    2.请求权限

      如果应用尚未所需的权限,则需要调用requestPermissions()方法请求权限。这个方法是异步执行的,它会调用一个系统标准Android对话框。会在onRequestPermissionsResult中获得授权结果:

// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) { // Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS)) { // Show an expanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission. } else { // No explanation needed, we can request the permission.

ActivityCompat.requestPermissions
(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS); // MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
}

  对于shouldShowRequsetPermissionRationale()方法的使用解释:

  •   这里需要指出的是当第一次被用户拒绝之后,在第二次申请权限的时候可以使用shouldShowRequsetPermissionRationale()来帮助判断用户是否需要解释授权,若应用之前请求过此权限但用户拒绝了请求,该方法将返回true。

  对于这个方法的使用场景,官方文档原话:

    您可以采用的一个方法是仅在用户已拒绝某项权限请求时提供解释。如果用户继续尝试使用需要某项权限的功能,但继续拒绝权限请求,则可能表明用户不理解应用为什么需要此权限才能提供相关功能。对于这种情况,比较好的做法是显示解释。

  •    在第二次请求权限时系统对话框会有Don't ask again的选项,若用户选择了这个选项,那么在下次请求时shouldShowRequsetPermissionRationale()也会返回false。

    3.申请结果回调处理

  当用户响应时,系统将调用应用的 onRequestPermissionsResult() 方法,向其传递用户响应。您的应用必须重写该方法,以了解是否已获得相应权限。回调会将您传递的相同请求代码传递给 requestPermissions()

  

@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { // permission was granted, yay! Do the
// contacts-related task you need to do.
          
} else { // permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
} // other 'case' lines to check for other
// permissions this app might request
}
}

总结:到这里,需要说的是,当用户拒绝了权限申请后,应用应该采取适当的操作,例如弹出一个对话框解释为什么需要该权限。

当用户指示系统不再要求提供该权限时,这种情况下,应用使用requestPermissions()再次请求权限时,系统都会立即拒绝请求,并回调onRequestPermissionsResult()回调方法,并传递PERMISSION_DENIED。这意味着当您调用 requestPermissions() 时,您不能假设已经发生与用户的任何直接交互。

封装后的基类代码:BaseActivity

public class BaseActivity extends AppCompatActivity {

    private HashMap<Integer,RequestPermissionCallback> permissionCallbackHashMap=new HashMap<>();
private List<Integer> requestCodes=new ArrayList<>(); /**
* 申请成功的回调接口
*/
public interface RequestPermissionCallback{
void onRequestCallback();
} private String[] findDeniedPermissions(String...permissions){
List<String> permissionList=new ArrayList<>();
for (String perm:permissions){
if (ContextCompat.checkSelfPermission(this,perm)!=
PackageManager.PERMISSION_GRANTED){
permissionList.add(perm);
}
}
return permissionList.toArray(new String[permissionList.size()]);
} protected void hasPermissions(int requestCode,RequestPermissionCallback callback,String... permissions){
permissionCallbackHashMap.put(requestCode,callback);
requestCodes.add(requestCode);
String[] deniedPermissions=findDeniedPermissions(permissions);
if (deniedPermissions.length>0){
ActivityCompat.requestPermissions(this,permissions,requestCode);
}else {
callback.onRequestCallback();
}
} private boolean verifyPermissions(int [] grantResults){
for (int result:grantResults){
if (result!=PackageManager.PERMISSION_GRANTED){
return false;
}
}
return true;
} @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
boolean grandResult=verifyPermissions(grantResults);
for (int code:requestCodes){
if (code==requestCode){
if (grandResult){
permissionCallbackHashMap.get(code).onRequestCallback();
}else {
ActivityCompat.requestPermissions(this,permissions, requestCode);
}
}
}
}
}

BaseFragment:

public abstract class BaseFragment extends Fragment {
private HashMap<Integer,BaseActivity.RequestPermissionCallback> permissionCallbackHashMap=new HashMap<>();
private List<Integer> requestCodes=new ArrayList<>();
private Context mContext; @Override
public void onAttach(Context context) {
super.onAttach(context);
this.mContext=mContext;
} public Context getmContext() {
return mContext;
} /**
* 申请成功的回调接口
*/
public interface RequestPermissionCallback{
void onRequestCallback();
} private String[] findDeniedPermissions(String...permissions){
List<String> permissionList=new ArrayList<>();
for (String perm:permissions){
if (ContextCompat.checkSelfPermission(getmContext(),perm)!=
PackageManager.PERMISSION_GRANTED){
permissionList.add(perm);
}
}
return permissionList.toArray(new String[permissionList.size()]);
} protected void hasPermissions(int requestCode, BaseActivity.RequestPermissionCallback callback, String... permissions){
permissionCallbackHashMap.put(requestCode,callback);
requestCodes.add(requestCode);
String[] deniedPermissions=findDeniedPermissions(permissions);
if (deniedPermissions.length>0){
/**
* 这里要直接用Fragment的requestPermissions(),
* 使用ActivityCompat的requestPermissions()会回调到Activity的onRequestPermissionsResult()
*/
requestPermissions(permissions,requestCode);
}else {
callback.onRequestCallback();
}
} private boolean verifyPermissions(int [] grantResults){
for (int result:grantResults){
if (result!=PackageManager.PERMISSION_GRANTED){
return false;
}
}
return true;
} @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
boolean grandResult=verifyPermissions(grantResults);
for (int code:requestCodes){
if (code==requestCode){
if (grandResult){
permissionCallbackHashMap.get(code).onRequestCallback();
}else {
requestPermissions(permissions, requestCode);
}
}
}
}
}

  ※这里有一个坑:在Fragment中申请权限要用Fragment自带的requestPermissions,否则会导致结果回调到activity的onRequestPermissionsResult而收不到申请结果。

   关于Fragment嵌套的情况看:http://blog.csdn.net/qfanmingyiq/article/details/52561658

如果应用非要申请某个权限的话,可以用以下代码:(慎用,有引起用户反感的可能):

public abstract class BaseActivity extends AppCompatActivity {

    private HashMap<Integer,RequestPermissionCallback> permissionCallbackHashMap=new HashMap<>();
private List<Integer> requestCodes=new ArrayList<>(); /**
* 申请成功的回调接口
*/
public interface RequestPermissionCallback{
void onRequestCallback();
} private String[] findDeniedPermissions(String...permissions){
List<String> permissionList=new ArrayList<>();
for (String perm:permissions){
if (ContextCompat.checkSelfPermission(this,perm)!=
PackageManager.PERMISSION_GRANTED){
permissionList.add(perm);
}
}
return permissionList.toArray(new String[permissionList.size()]);
} protected void hasPermissions(int requestCode, RequestPermissionCallback callback, String... permissions){
permissionCallbackHashMap.put(requestCode,callback);
requestCodes.add(requestCode);
String[] deniedPermissions=findDeniedPermissions(permissions);
if (deniedPermissions.length>0){
ActivityCompat.requestPermissions(this,permissions,requestCode);
}else {
callback.onRequestCallback();
}
} private boolean verifyPermissions(int [] grantResults){
for (int result:grantResults){
if (result!=PackageManager.PERMISSION_GRANTED){
return false;
}
}
return true;
} /**
* 当被用户拒绝授权并且出现不再提示时(小米在第一拒绝后再次申请就不会出现请求对话框了,不过对于call phone权限不同,这点再看),
* shouldShowRequestPermissionRationale也会返回false,若实在必须申请权限时可以使用方法检测,
* 可能会引起用户的厌恶感,慎用
* @param permissions
* @return
*/
protected boolean verifyShouldShowRequestPermissions(String[] permissions){
for (String permission:permissions){
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,permission)){
return false;
}
}
return true;
} /**
* 显示提示信息,与verifyShouldShowRequestPermissions(String[] permissions)搭配使用,提醒用户需要授权,并打开应用详细设置页面
*
* @since 2.5.0
*
*/
protected void showMissingPermissionDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("提示");
builder.setMessage("当前应用缺少必要权限。\\n\\n请点击\\\"设置\\\"-\\\"权限\\\"-打开所需权限。"); // // 拒绝, 退出应用
// builder.setNegativeButton(R.string.cancel,
// listener
// ); builder.setPositiveButton(R.string.setting,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
startAppSettings();
}
}); builder.setCancelable(false); builder.show();
} /**
* 启动应用的设置
*
* @since 2.5.0
*
*/
private void startAppSettings() {
Intent intent = new Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
} @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
boolean grandResult=verifyPermissions(grantResults);
for (int code:requestCodes){
if (code==requestCode){
if (grandResult){
permissionCallbackHashMap.get(code).onRequestCallback();
}else {
if (verifyShouldShowRequestPermissions(permissions)){
final int code1=code;
final String[] permission=permissions;
AlertDialog.Builder builder=new AlertDialog.Builder(this);
builder.setMessage("定位功能需要定位权限,请授权。")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
ActivityCompat.requestPermissions(BaseActivity.this,permission, code1);
}
});
}else {
showMissingPermissionDialog();
}
}
}
}
}
}

这里需要指出的是,shouldShowRequestPermissionRationale()被放到onRequestPermissionsResult中进行判断,这样就能在系统直接返回PERMISSION_DENIED这个回调结果时判断是否应该弹出解释窗口或者在用户选择了不再提示之后弹出应用设置页面。

这里有一段关于官方文档的原话:

  在某些情况下,您可能需要帮助用户了解您的应用为什么需要某项权限。例如,如果用户启动一个摄影应用,用户对应用要求使用相机的权限可能不会感到吃惊,但用户可能无法理解为什么此应用想要访问用户的位置或联系人。在请求权限之前,不妨为用户提供一个解释。请记住,您不需要通过解释来说服用户;如果您提供太多解释,用户可能发现应用令人失望并将其移除。

所以这个方法要慎用。

代码地址:https://github.com/liberty2015/PermissionStudy

参考:

  http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0830/3387.html

  http://blog.csdn.net/lmj623565791/article/details/50709663

  http://blog.csdn.net/qfanmingyiq/article/details/52561658

  https://developer.android.com/guide/topics/security/permissions.html

  https://developer.android.com/training/permissions/requesting.html

Android 6.0 权限知识学习笔记的更多相关文章

  1. Objective-c基础知识学习笔记

    Objective-c基础知识学习笔记(一) 一直有记录笔记的习惯.但非常久没分享一些东西了,正好上半年開始学习IOS了,如今有空写点.因开发须要,公司特意为我们配置了几台新MAC.还让我们自学了2周 ...

  2. Google Android 6.0 权限完全解析

    注:本文只针对Google原生Android系统有效, 小米魅族等手机有自己的权限机制, 可能不适用 一.运行时权限的变化及特点 新的权限机制更好的保护了用户的隐私,Google将权限分为两类,一类是 ...

  3. Android开源项目SlidingMenu本学习笔记(两)

    我们已经出台SlidingMenu使用:Android开源项目SlidingMenu本学习笔记(一个),接下来再深入学习下.依据滑出项的Menu切换到相应的页面 文件夹结构: watermark/2/ ...

  4. Android 6.0 权限申请辅助 ----PermissionsHelper

    Android 6.0 权限申请辅助 ----PermissionsHelper 项目地址:https://github.com/didikee/PermissionsHelper Android 的 ...

  5. ## Android 6.0 权限申请 ##

    Android 6.0 权限申请 1. 以前的权限申请(sdk<23) 直接在AndroidManifest.xml中申明即可: <uses-permission android:name ...

  6. Android 6.0权限管理

    Android 6.0权限管理 关于权限管理 Android6.0 发布之后,Android 的权限系统被重新设计.在 23 之前 App 的权限只会在用户安装的时候询问一次,App一旦安装后就可以使 ...

  7. MySQL索引知识学习笔记

    目录 一.索引的概念 二.索引分类 三.索引用法 四 .索引架构简介 五.索引适用的情况 六.索引不适用的情况 继我的上篇博客:Oracle索引知识学习笔记,再记录一篇MySQL的索引知识学习笔记,本 ...

  8. android:Android 6.0权限控制代码封装

    新建的Activity类可以继承这个Activity,这个类封装了关于新版的权限处理相关的代码 使用方法: package com.glsite.phone; import android.conte ...

  9. Android 5.x SEAndroid/SElinux内核节点的读写权限【学习笔记】

    本文转载自:http://blog.csdn.net/tung214/article/details/44461985 Android 5.0下,因为采取了SEAndroid/SElinux的安全机制 ...

随机推荐

  1. TODO:搭建Laravel VueJS SemanticUI

    TODO:搭建Laravel VueJS SemanticUI Laravel是一套简洁.优雅的PHP开发框架(PHP Web Framework).可以让你从面条一样杂乱的代码中解脱出来:它可以帮你 ...

  2. Enterprise Solution 3.1 企业应用开发框架 .NET ERP/CRM/MIS 开发框架,C/S架构,SQL Server + ORM(LLBL Gen Pro) + Infragistics WinForms

    行业:基于数据库的制造行业管理软件,包含ERP.MRP.CRM.MIS.MES等企业管理软件 数据库平台:SQL Server 2005或以上 系统架构:C/S 开发技术 序号 领域 技术 1 数据库 ...

  3. 【.net 深呼吸】跨应用程序域执行程序集

    应用程序域,你在网上可以查到它的定义,凡是概念性的东西,大伙儿只需要会搜索就行,内容看了就罢,不用去记忆,更不用去背,“名词解释”是大学考试里面最无聊最没水平的题型. 简单地说,应用程序域让你可以在一 ...

  4. [.NET] 利用 async & await 进行异步 IO 操作

    利用 async & await 进行异步 IO 操作 [博主]反骨仔 [出处]http://www.cnblogs.com/liqingwen/p/6082673.html  序 上次,博主 ...

  5. fir.im Weekly - 关于 iOS10 适配、开发、推送的一切

    "小程序"来了,微信变成名副其实的 Web OS,新一轮的Web App 与Native App争论四起.程序员对新技术永远保持灵敏的嗅觉和旺盛的好奇心,@李锦发整理了微信小程序资 ...

  6. 如何使用swing创建一个BeatBox

    首先,我们需要回顾一些内容(2017-01-04 14:32:14): 1.Swing组件 Swing的组件(component,或者称之为元件),是较widget更为正确的术语,它们就是会放在GUI ...

  7. C++常见笔试面试要点以及常见问题

    1. C++常见笔试面试要点: C++语言相关: (1) 虚函数(多态)的内部实现 (2) 智能指针用过哪些?shared_ptr和unique_ptr用的时候需要注意什么?shared_ptr的实现 ...

  8. background例子

  9. ionic第二坑——ionic 上拉菜单(ActionSheet)安卓样式坑

    闲话不说,先上图: 这是IOS上的显示效果,代码如下: HTML部分: <body ng-app="starter" ng-controller="actionsh ...

  10. 怎样在Dos里切换盘符

    一:在Dos里切换盘符 a:在电脑左下角右击显示图片;(我用的是win10系统,其他系统类似) b:点击运行,输入cmd; c:点击确定: d:输入盘符:(如f:) 或F: 只写字母,不写分号是不行的 ...