转载自:http://blog.csdn.net/AndrExpert/article/details/53331836?locationNum=8&fps=1
探讨Android 6.0及以上新权限系统的检测与处理
作者:
蒋东国
时间:
2016年11月24日 星期四
应用来源:
lcb APP(测试机型:huawei 4X(6.0)/Samsung Note4(Android 5.0.1))
博客地址:
http://blog.csdn.net/andrexpert/article/details/53331836
情景再现:“最近在开发一个地图导航软件,由于之前的测试都是部署在三星Note4(5.0.1)上,相关功能基本正常。当自己换到荣耀4X(Android 6.0)时,软件虽然没有异常退出,但却无法定位,通过查看Logcat打印出来的日志,发现这是因为APP部署到6.0以上系统后,Android系统没有授予相关位置权限才会报“Could not open database,(OS error -13:Permission denied)”错误。”
从Google官方文档可知,Android系统升级到6.0后,它的权限系统被重新设计。相比原来新安装的APP系统会一次性授予所有权限和用户无法管理APP权限的不足,新的权限系统不再允许新安装的APP一次性获得所有权限,APP必须在运行时一个一个地询问用户授予权限,甚至有时候都不会主动申请用户授权,开发者不得不自己去检测和请求用户授予来获得权限。那么当我们的APP部署到Android 6.0以上系统的终端时,某些功能没有获得相关权限或者用户拒绝授予权限又会出现什么问题呢?这里的异常情况有两种:
(1) APP暂时不支持6.0以上系统,即APP的AndroidManifest.xml配置文件<uses-sdk../>
中的android:targetSdkVersion属性值<23时(Android 6.0即SDK版本23),系统会认为新安装的APP还没有支持新的权限系统而不会异常退出,当然APP相关的功能也不会正常运行。
(2) APP已经支持6.0以上系统,即APP的 AndroidManifest.xml配置文件<uses-sdk../>中
android:targetSdkVersion属性值>=23时,如果APP在运行时没有获得相关的权限,将会异常退出。
1. 让你的APP支持新的运行时权限
Android权限系统被重新设计后,系统的安全性自然提高不少,用户的相关数据也得到了很好的保护。但是对于开发者而言,如果其APP还没有引入支持新的权限系统,当APP运行在Android6.0以上系统时,很可能导致很多问题,这对于一款APP来说可能是致命的。因此,谷歌在API 23中引入了ContextWrapper.checkSelfPermission和Activity.requestPermissions两个方法分别用来检查所请求权限的授予情况和请求用户授予该权限。当然,之前很多人都认为使用权限的检查都是用checkPermission、checkCallingOrSelfPermission来实现,其实,这两个方法只是检测你的APP是否声明了该权限,只要你声明了运行时请求的权限,它们都将返回PERMISSION_GRANTED,最后自然也无法在APP运行时真正检查到是否拥有该权限。
为了更好的理解和使用ContextWrapper.checkSelfPermission、Activity.requestPermissions来进行运行时权限的检测和请求,我根据需求将其进行了封装,即是自己的总结,也方便了他人,具体内容如下:
(1) APP请求单个权限,封装的方法为isPermissionGranted,它需要传递两个参数:
permissionName参数为请求的权限,比如发送短信为Manifest.permission.SEND_SMS;questCode参数为请求标志,将作为该权限的判断标志。
[java] view plain copy protectedboolean isPermissionGranted(String permissionName,int questCode){ //6.0以下系统,取消请求权限 if(Build.VERSION.SDK_INT< Build.VERSION_CODES.M){ returntrue; } //判断是否需要请求允许权限,否则请求用户授权 inthasPermision = checkSelfPermission(permissionName); if(hasPermision != PackageManager.PERMISSION_GRANTED) { requestPermissions(newString[] { permissionName}, questCode); returnfalse; } returntrue; }
(2) APP同时请求多个权限,即批量请求权限,常见于APP第一次安装时需要用户批量授予多个权限,这样能够提高APP后续的用户体验,封装的方法为isPermissionsAllGranted,它需要传递两个参数:permArray参数为批量请求的权限字符串数组,questCode参数为请求标志,将作为这次请求的判断标志。
[java] view plain copy protected boolean isPermissionsAllGranted(String[] permArray,intquestCode){ //6.0以下系统,取消请求权限 if(Build.VERSION.SDK_INT <Build.VERSION_CODES.M){ returntrue; } //获得批量请求但被禁止的权限列表 List<String> deniedPerms = newArrayList<String>(); for(int i=0;permArray!=null&&i<permArray.length;i++){ if(PackageManager.PERMISSION_GRANTED !=checkSelfPermission(permArray[i])){ deniedPerms.add(permArray[i]); } } int denyPermNum = deniedPerms.size(); //进行批量请求 if(denyPermNum != 0){ requestPermissions(deniedPerms.toArray(newString[denyPermNum]),questCode); returnfalse; } return true; }
注意:在进行权限检测、请求时,需要对当前部署的终端进行系统版本检查,判断是否为Android6.0以上系统,否则会报“method can not reserved”异常,因为checkSelfPermission和requestPermissions是API 23才引入的方法。另外,Android 6.0以下的系统无需进行权限检测,因为它仍然使用老的权限系统,APP安装时会默认授予运行时所有的权限。
(3) 获得权限用户授予情况
由上述(1)、(2)可知,通过封装的两个方法,我们可以很容易的检测某个权限或多个权限是否被用于授予,以及去请求某个或多个权限需要用户授予。那么,我们又如何知道用户最终的授权情况,和针对不同的授权情况做相应的处理呢?答案是:onRequestPermissionsResult方法。onRequestPermissionsResult是Activity的一个方法,通过覆写该方法并结合权限标识码requestCode来判断相应权限授予结果,代码如下:
[java] view plain copy publicvoid onRequestPermissionsResult(int requestCode, String[]permissions, int[] grantResults) { if(grantResults.length==0){ return; } switch(requestCode) { caseConstants.QUEST_CODE_SEND_SMS: if(grantResults[0] != PackageManager.PERMISSION_GRANTED) { //用户拒绝授予发送短信权限,弹出一个警告对话框 popAlterDialog(); } break; caseConstants.QUEST_CODE_ALL: //用户拒绝授予请求所有或某一项权限,弹出一个经过对话框 doPermissionAll(Constants.permArray,grantResults); break; default: super.onRequestPermissionsResult(requestCode,permissions, grantResults); break; } }
2. Demo实战剖析
为了进一步理解Android 6.0以上系统新权限系统的检查处理,我写了一个小Demo,也算是对封装的两个方法的演示。整个Demo项目以BaseActivity.class、MainActivity.class、AndroidManifest.xml为核心,代码介绍如下:
(1) BaseActivity.class
由于checkSelfPermission、requestPermissions、onRequestPermissionsResult方法属于Activity,为了防止在不同的Activity都要写一个套权限检测方法导致代码冗余,这里封装一个BaseActivity,当我们在某个Activity需要请求某个权限时,只需继承该父Activity即可使用相关的权限检查方法和获得处理,代码如下:
[java] view plain copy /** *@dscrible Activity基类 * * Created by jiangdongguo on 2016-11-25 上午9:42:08 */ public abstract class BaseActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); onCreateView(); init(); } abstract void onCreateView(); abstract void init(); protected boolean isPermissionGranted(String permissionName,int questCode){ if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M){ return true; } //判断是否需要请求允许权限 int hasPermision = checkSelfPermission(permissionName); if (hasPermision != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[] { permissionName }, questCode); return false; } return true; } protected boolean isPermissionsAllGranted(String[] permArray,int questCode){ if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M){ return true; } //获得批量请求但被禁止的权限列表 List<String> deniedPerms = new ArrayList<String>(); for(int i=0;permArray!=null&&i<permArray.length;i++){ if(PackageManager.PERMISSION_GRANTED != checkSelfPermission(permArray[i])){ deniedPerms.add(permArray[i]); } } //进行批量请求 int denyPermNum = deniedPerms.size(); if(denyPermNum != 0){ requestPermissions(deniedPerms.toArray(new String[denyPermNum]),questCode); return false; } return true; } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if(grantResults.length==0){ return; } switch (requestCode) { case Constants.QUEST_CODE_LOCTION: if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { popAlterDialog("位置","位置信息权限被禁止,将导致定位失败。。是否开启该权限?(步骤:应用信息->权限->'勾选'位置)"); }else{ showShortMsg("恭喜,用户已经授予位置权限"); } break; case Constants.QUEST_CODE_CAMERA: if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { popAlterDialog("相机","摄像头使用权限被禁止,手电筒无法正常使用。是否开启该权限?(步骤:应用信息->权限->'勾选'相机)"); }else{ showShortMsg("恭喜,用户已经授予相机权限"); } break; case Constants.QUEST_CODE_SEND_SMS: if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { popAlterDialog("短信","发送短信权限被禁止,无法使用反馈/建议功能。是否开启该权限?(步骤:应用信息->权限->'勾选'短信)"); }else{ showShortMsg("恭喜,用户已经授予短信权限"); } break; case Constants.QUEST_CODE_ALL: doPermissionAll(Constants.permArray,grantResults); break; case Constants.QUEST_CODE_CALL_PHONE: if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { popAlterDialog("拨打电话","拨打电话权限被禁止,无法使用拨打电话功能。是否开启该权限?(步骤:应用信息->权限->'勾选'电话)"); }else{ showShortMsg("恭喜,用户已经授予所有权限"); } break; default: super.onRequestPermissionsResult(requestCode, permissions, grantResults); break; } } private void doPermissionAll(String[] permissions, int[] grantResults) { int grantedPermNum = 0; int totalPermissons = permissions.length; int totalResults = grantResults.length; if(totalPermissons == 0 || totalResults == 0){ return; } Map<String,Integer> permResults = new HashMap<String,Integer>(); //初始化Map容器,用于判断哪些权限被授予 for(String perm:Constants.permArray){ permResults.put(perm,PackageManager.PERMISSION_DENIED); } //根据授权的数目和请求授权的数目是否相等来判断是否全部授予权限 for(int i=0;i<totalResults;i++){ permResults.put(permissions[i],grantResults[i]); if(permResults.get(permissions[i]) == PackageManager.PERMISSION_GRANTED){ grantedPermNum ++; } Log.d("Debug","权限:"+permissions[i]+"-->"+grantResults[i]); } if (grantedPermNum == totalPermissons) { //用于授予全部权限 }else{ showShortMsg( "批量申请权限失败,将会影响正常使用!"); } } private void showShortMsg(String msg) { Toast.makeText(this,msg, Toast.LENGTH_SHORT).show(); } private void popAlterDialog(final String msgFlg, String msgInfo) { new AlertDialog.Builder(BaseActivity.this) .setTitle("使用警告") .setMessage(msgInfo) .setNegativeButton("取消", new DialogInterface.OnClickListener(){ @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }) .setPositiveButton("设置",new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { //前往应用详情界面 try { Uri packUri = Uri.parse("package:"+getPackageName()); Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,packUri); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); BaseActivity.this.startActivity(intent); } catch (Exception e) { showShortMsg("跳转失败"); } dialog.dismiss(); } }).create().show(); } }
(2) MainActivity.class:继承于BaseActivity,发起权限检测
[java] view plain copy /** *@dscrible 分别请求短信、位置等权限 * * Created by jiangdongguo on 2016-11-25 上午9:45:20 */ public class MainActivity extends BaseActivity implements OnClickListener{ private Button mCheckContactBtn; private Button mCheckLocationBtn; private Button mCheckAllBtn; @Override void onCreateView() { setContentView(R.layout.activity_main); initLayout(); } private void initLayout() { mCheckContactBtn = (Button)findViewById(R.id.check_contacts_btn); mCheckLocationBtn = (Button)findViewById(R.id.check_location_btn); mCheckAllBtn = (Button)findViewById(R.id.check_all_permission_btn); mCheckContactBtn.setOnClickListener(this); mCheckLocationBtn.setOnClickListener(this); mCheckAllBtn.setOnClickListener(this); } @Override void init() { } @Override public void onClick(View v) { int vId = v.getId(); switch (vId) { case R.id.check_contacts_btn: //检测发送短信权限 isPermissionGranted(Manifest.permission.WRITE_CONTACTS, Constants.QUEST_CODE_SEND_SMS); break; case R.id.check_location_btn: //检测位置信息权限 isPermissionGranted(Manifest.permission.ACCESS_COARSE_LOCATION, Constants.QUEST_CODE_LOCTION); break; case R.id.check_all_permission_btn: //批量检测短信、位置、相机、电话使用权限 isPermissionsAllGranted(Constants.permArray,Constants.QUEST_CODE_ALL); break; default: break; } } }(3) AndroidManifest.xml:声明要请求的权限,设置android:targetSdkVersion支持Android6.0+新权限系统,即android:targetSdkVersion="23"。
[html] view plain copy <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.mandroidpermissiondemo" android:versionCode="1" android:versionName="1.0" > <uses-permission android:name="android.permission.WRITE_CONTACTS"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.READ_SMS"/> <uses-permission android:name="android.permission.CALL_PHONE"/> <uses-sdk android:minSdkVersion="18" android:targetSdkVersion="23" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity " android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
(4) Constants.class:常量类
[java] view plain copy /** *@dscrible 常量和临时变量类 * * Created by jiangdongguo on 2016-10-31 上午11:10:27 */ public class Constants { /**位置信息权限请求标志*/ public static final int QUEST_CODE_LOCTION = 1; /**发送短信权限请求标志*/ public static final int QUEST_CODE_SEND_SMS = 2; /**摄像头权限标志*/ public static final int QUEST_CODE_CAMERA = 3; /**批量请求权限*/ public static final int QUEST_CODE_ALL = 4; /**拨打电话权限*/ public static final int QUEST_CODE_CALL_PHONE = 5; //要申请的权限 public static final String[] permArray = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.SEND_SMS, Manifest.permission.CAMERA, Manifest.permission.CALL_PHONE}; }效果演示:
码字不容易,转载请注明出处:http://blog.csdn.net/andrexpert/article/details/53331836
最后的话:“实际上,Android的权限系统中涵盖了两类权限:PROTECTION_NORMAL、非PROTECTION_NORMAL。其中,PROTECTION_NORML类权限在AndroidManifest.xml中声明后,在APP安装时,系统将无需检查权限而直接授予,且用户不可取消,如蓝牙、网络、闪光灯、NFC、WIFI等等;非PROTECTION_NORMAL类权限在AndroidManifest.xml中声明,在安装APP时,系统将会询问APP每次请求的权限,用户可取消此类权限。系统有时候不会弹出权限授权对话框,需要APP主动检查有没有该类权限,并弹出授权提示对话框,我们处理的就是这类权限。另外,Android权限系统是以组的形式管理权限,比如发送短信、接收短信同属一组权限,一旦其他一个获得授予,其他同组组员都得到了授权。”
关于资料与Demo:Android6.0新权限系统Demo
