概要 android的权限系统一直是首要的安全概念,因为这些权限只在安装的时候被询问一次。 一旦安装了,app可以在用户毫不知晓的情况下访问权限内的所有东西。 难怪一些坏蛋利用这个缺陷恶意收集用户数据用来做坏事了!
android小组也知道这事儿。7年了!权限系统终于被重新设计了。 在android6.0棉花糖 ,app将不会在安装的时候授予权限 。取而代之的是,app不得不在运行时一个一个询问用户授予权限 。
注意权限询问对话框不会自己弹出来 。开发者不得不自己调用 。如果开发者要调用的一些函数需要某权限而用户又拒绝授权的话,函数将抛出异常直接导致程序崩溃。
那么问题就来了怎样解决呢
这个新的运行时权限仅当我们设置targetSdkVersion to 23(这意味着你已经在23上测试通过了)才起作用,当然还要是M系统的手机。app在6.0之前的设备依然使用旧的权限系统。
所以我们可以设置targetSdkVersion为22
但这样毕竟不是好的方式 我们还是好好学学新版的权限这样使用
正文 新版的权限可以分为两大类普通权限和运行时权限
运行时权限需要询问用户
普通权限只要在AndroidManifest.xml中声明就好了,安装应用时会自动赋予
普通权限 普通权限包含以下权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 android.permission.ACCESS_LOCATION_EXTRA_COMMANDS android.permission.ACCESS_NETWORK_STATE android.permission.ACCESS_NOTIFICATION_POLICY android.permission.ACCESS_WIFI_STATE android.permission.ACCESS_WIMAX_STATE android.permission.BLUETOOTH android.permission.BLUETOOTH_ADMIN android.permission.BROADCAST_STICKY android.permission.CHANGE_NETWORK_STATE android.permission.CHANGE_WIFI_MULTICAST_STATE android.permission.CHANGE_WIFI_STATE android.permission.CHANGE_WIMAX_STATE android.permission.DISABLE_KEYGUARD android.permission.EXPAND_STATUS_BAR android.permission.FLASHLIGHT android.permission.GET_ACCOUNTS android.permission.GET_PACKAGE_SIZE android.permission.INTERNET android.permission.KILL_BACKGROUND_PROCESSES android.permission.MODIFY_AUDIO_SETTINGS android.permission.NFC android.permission.READ_SYNC_SETTINGS android.permission.READ_SYNC_STATS android.permission.RECEIVE_BOOT_COMPLETED android.permission.REORDER_TASKS android.permission.REQUEST_INSTALL_PACKAGES android.permission.SET_TIME_ZONE android.permission.SET_WALLPAPER android.permission.SET_WALLPAPER_HINTS android.permission.SUBSCRIBED_FEEDS_READ android.permission.TRANSMIT_IR android.permission.USE_FINGERPRINT android.permission.VIBRATE android.permission.WAKE_LOCK android.permission.WRITE_SYNC_SETTINGS com.android.alarm.permission.SET_ALARM com.android.launcher.permission.INSTALL_SHORTCUT com.android.launcher.permission.UNINSTALL_SHORTCUT
运行时权限
权限组 权限
android.permission-group.CALENDAR
android.permission.READ_CALENDAR
android.permission.WRITE_CALENDAR
android.permission-group.CAMERA
android.permission.CAMERA
android.permission-group.CONTACTS
android.permission.READ_CONTACTS
android.permission.WRITE_CONTACTS
android.permission.GET_ACCOUNTS
android.permission-group.LOCATION
android.permission.ACCESS_FINE_LOCATION
android.permission.ACCESS_COARSE_LOCATION
android.permission-group.MICROPHONE
android.permission.RECORD_AUDIO
android.permission-group.PHONE
android.permission.READ_PHONE_STATE
android.permission.CALL_PHONE
android.permission.READ_CALL_LOG
android.permission.WRITE_CALL_LOG
com.android.voicemail.permission.ADD_VOICEMAIL
android.permission.USE_SIP
android.permission.PROCESS_OUTGOING_CALLS
android.permission-group.SENSORS
android.permission.BODY_SENSORS
android.permission-group.SMS
android.permission.SEND_SMS
android.permission.RECEIVE_SMS
android.permission.READ_SMS
android.permission.RECEIVE_WAP_PUSH
android.permission.RECEIVE_MMS
android.permission.READ_CELL_BROADCASTS
android.permission-group.STORAGE
android.permission.READ_EXTERNAL_STORAGE
android.permission.WRITE_EXTERNAL_STORAGE
从上图中我们可以看到 权限都被分了组 同一组的任何一个权限被授权了,其他权限也自动被授权。例如,一旦WRITE_CONTACTS被授权了,app也有READ_CONTACTS和GET_ACCOUNTS权限了。
是时候让我们的app支持新权限模型了,从设置compileSdkVersion and targetSdkVersion 为 23开始吧.
1 2 3 4 5 6 7 8 9 10 android { compileSdkVersion 23 defaultConfig { targetSdkVersion 23 } }
请求单个权限 假如我们要添加联系人
1 2 3 4 private void insertDummyContact () {}
下一步像以前一样在AndroidManifest.xml添加声明权限。
1 <uses-permission android:name ="android.permission.WRITE_CONTACTS" />
光是这样的话还是没有权限,所以我们要询问用户授权
定义全局变量
1 final private int REQUEST_CODE_ASK_PERMISSIONS = 123 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private void insertDummyContactWrapper () { if (Build.VERSION.SDK_INT >= 23 ) { int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS); if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String []{Manifest.permission.WRITE_CONTACTS}, REQUEST_CODE_ASK_PERMISSIONS); return ; }else { insertDummyContact(); } }else { insertDummyContact(); } }
如果已有权限,insertDummyContact()会执行。 否则,requestPermissions被执行来弹出请求授权对话框 不论用户同意还是拒绝,activity的onRequestPermissionsResult会被回调来通知结果(通过第三个参数: grantResults)
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public void onRequestPermissionsResult (int requestCode, String[] permissions, int [] grantResults) { switch (requestCode) { case REQUEST_CODE_ASK_PERMISSIONS: if (grantResults[0 ] == PackageManager.PERMISSION_GRANTED) { insertDummyContact(); } else { Toast.makeText(MainActivity.this , "通讯录没有写入权限" , Toast.LENGTH_SHORT).show(); } break ; default : super .onRequestPermissionsResult(requestCode, permissions, grantResults); } }
处理 用户点击了“不再提醒”的情况 如果用户拒绝某授权。下一次弹框,用户会有一个“不再提醒”的选项的来防止app以后继续请求授权。 如果这个选项在拒绝授权前被用户勾选了。下次为这个权限请求requestPermissions时,对话框就不弹出来了,结果就是,app啥都不干。 这将是很差的用户体验,用户做了操作却得不到响应。这种情况需要好好处理一下。 在请求requestPermissions前,我们通过activity的shouldShowRequestPermissionRationale方法来检查是否需要弹出请求权限的提示对话框
第一次请求权限时,用户拒绝了,下一次:shouldShowRequestPermissionRationale() 返回 true,应该显示一些为什么需要这个权限的说明
第二次请求权限时,用户拒绝了,并选择了“不在提醒”的选项时:shouldShowRequestPermissionRationale() 返回 false
设备的策略禁止当前应用获取这个权限的授权:shouldShowRequestPermissionRationale() 返回 false
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 private void insertDummyContactWrapper () { if (Build.VERSION.SDK_INT >= 23 ) { int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS); if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) { if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS)) { showMessageOKCancel("请从系统设置中开启访问通讯录的权限" , new DialogInterface .OnClickListener() { @Override public void onClick (DialogInterface dialog, int which) { if (Build.VERSION.SDK_INT >= 23 ) { requestPermissions(new String []{Manifest.permission.WRITE_CONTACTS}, REQUEST_CODE_ASK_PERMISSIONS); } } }); } else { requestPermissions(new String []{Manifest.permission.WRITE_CONTACTS}, REQUEST_CODE_ASK_PERMISSIONS); } } else { insertDummyContact(); } } else { insertDummyContact(); } } private void showMessageOKCancel (String message, DialogInterface.OnClickListener okListener) { new AlertDialog .Builder(IMChatActivity.this ) .setMessage(message) .setPositiveButton("确认" , okListener) .setNegativeButton("取消" , null ) .create() .show(); }
当一个权限第一次被请求和用户标记过不再提醒的时候,我们写的对话框被展示。 最后一种情况,onRequestPermissionsResult 会收到PERMISSION_DENIED ,系统询问对话框不展示。
一次请求多个权限 当然了有时候需要好多权限,可以用上面方法一次请求多个权限。 不要忘了为每个权限检查“不再提醒”的设置。 修改后的代码:
添加全局常量
1 final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124 ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 private void insertDummyContactWrapper () { if (Build.VERSION.SDK_INT >= 23 ) { List<String> permissionsNeeded = new ArrayList <String>(); final List<String> permissionsList = new ArrayList <String>(); if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION)) { permissionsNeeded.add("GPS" ); } if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS)) { permissionsNeeded.add("读取通讯录" ); } if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS)) { permissionsNeeded.add("写入通讯录" ); } if (permissionsList.size() > 0 ) { if (permissionsNeeded.size() > 0 ) { String message = "应用需要以下权限,请手动打开:" + permissionsNeeded.get(0 ); for (int i = 1 ; i < permissionsNeeded.size(); i++) { message += ", " + permissionsNeeded.get(i); } showMessageOKCancel(message, new DialogInterface .OnClickListener() { @Override public void onClick (DialogInterface dialog, int which) { if (Build.VERSION.SDK_INT >= 23 ) { requestPermissions(permissionsList.toArray(new String [permissionsList.size()]), REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS); } } }); return ; } requestPermissions(permissionsList.toArray(new String [permissionsList.size()]), REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS); return ; } insertDummyContact(); } else { insertDummyContact(); } } private boolean addPermission (List<String> permissionsList, String permission) { if (Build.VERSION.SDK_INT >= 23 ) { if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) { permissionsList.add(permission); if (!shouldShowRequestPermissionRationale(permission)) { return false ; } } return true ; } return true ; } private void showMessageOKCancel (String message, DialogInterface.OnClickListener okListener) { new AlertDialog .Builder(IMChatActivity.this ) .setMessage(message) .setPositiveButton("确认" , okListener) .setNegativeButton("取消" , null ) .create() .show(); } @Override public void onRequestPermissionsResult (int requestCode, String[] permissions, int [] grantResults) { switch (requestCode) { case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS: { Map<String, Integer> perms = new HashMap <String, Integer>(); perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED); perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED); perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED); for (int i = 0 ; i < permissions.length; i++) { perms.put(permissions[i], grantResults[i]); } if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED && perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED && perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) { insertDummyContact(); } else { Toast.makeText(IMChatActivity.this , "没有赋予某些权限" , Toast.LENGTH_SHORT).show(); } } break ; default : super .onRequestPermissionsResult(requestCode, permissions, grantResults); } }
用兼容库来做兼容(非必需) 以上代码是通过判断SDK的版本来调用不同的方法来兼容不同的版本。当然也可以使用兼容包。
我建议用v4兼容库,已对这个做过兼容,用这个方法代替:
ContextCompat.checkSelfPermission() 被授权函数返回PERMISSION_GRANTED,否则返回PERMISSION_DENIED ,在所有版本都是如此。
ActivityCompat.requestPermissions() 这个方法在M之前版本调用,OnRequestPermissionsResultCallback 直接被调用,带着正确的 PERMISSION_GRANTED或者 PERMISSION_DENIED 。
ActivityCompat.shouldShowRequestPermissionRationale() 在M之前版本调用,永远返回false。 用v4包的这三方法,完美兼容所有版本!这个方法需要额外的参数,Context or Activity。
别的就没啥特别的了。下面是代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 private void insertDummyContactWrapper () { int hasWriteContactsPermission = ContextCompat.checkSelfPermission(MainActivity.this ,Manifest.permission.WRITE_CONTACTS); if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) { if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this ,Manifest.permission.WRITE_CONTACTS)) { showMessageOKCancel("必须允许访问通讯录" , new DialogInterface .OnClickListener() { @Override public void onClick (DialogInterface dialog, int which) { ActivityCompat.requestPermissions( MainActivity.this , new String [] {Manifest.permission.WRITE_CONTACTS}, REQUEST_CODE_ASK_PERMISSIONS ); } }); return ; } ActivityCompat.requestPermissions(MainActivity.this , new String [] {Manifest.permission.WRITE_CONTACTS}, REQUEST_CODE_ASK_PERMISSIONS); return ; } insertDummyContact(); }
我们也可以在Fragment中使用,用v13兼容包:FragmentCompat.requestPermissions()FragmentCompat.shouldShowRequestPermissionRationale()。
第三方库简化代码 以上代码真尼玛复杂。 为解决这事,有许多第三方库已经问世了。
项目没用Rxjava 建议用 hotchemi’s PermissionsDispatcher 。
如果项目用了Rxjava 更建议用RxPermissions
简单实例
添加依赖
1 2 3 compile 'io.reactivex.rxjava2:rxjava:2.0.5' compile 'io.reactivex.rxjava2:rxandroid:2.0.1' compile 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
代码
1 2 3 4 5 6 7 8 9 10 11 12 private void questAllPersission () { RxPermissions rxPermissions = new RxPermissions (this ); rxPermissions .request(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE) .subscribe(granted -> { if (granted) { Toast.makeText(LoginActivity.this , "授权成功" , Toast.LENGTH_SHORT).show(); } else { Toast.makeText(LoginActivity.this , "授权失败 软件将不能正常使用" , Toast.LENGTH_SHORT).show(); } }); }
上面用到了Lambda表达式 具体参见:Android开发使用Lambda表达式
结论建议 我相信你对新权限模型已经有了清晰的认识。我相信你也意识到了问题的严峻。
但是你没得选择。新运行时权限已经在棉花糖中被使用了。我们没有退路。我们现在唯一能做的就是保证app适配新权限模型.
欣慰的是只有少数权限需要运行时权限模型。 大多数常用的权限,例如,网络访问,属于普通权限 在安装时自动会授权,当然你要声明,以后无需检查。因此,只有少部分代码你需要修改。
两个建议:
1.严肃对待新权限模型
2.如果你代码没支持新权限,不要设置targetSdkVersion 23 。尤其是当你在Studio新建工程时,不要忘了修改!
Kotlin 申请权限
1 2 3 4 5 6 7 8 9 10 ActivityCompat.requestPermissions( this , arrayOf( Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_PHONE_STATE ), REQUEST_CODE_ASK_PERMISSIONS )
回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 override fun onRequestPermissionsResult (requestCode: Int , permissions: Array <out String >, grantResults: IntArray ) { super .onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { REQUEST_CODE_ASK_PERMISSIONS -> { for (i in 0 until permissions.size) { var mpermission = permissions.get (i) var mgrant = grantResults.get (i) if (mgrant == PackageManager.PERMISSION_GRANTED) { } else { when (mpermission){ Manifest.permission.READ_EXTERNAL_STORAGE->{ Toasty.warning(mContext, "扩展存储授权失败" ).show() } Manifest.permission.CAMERA->{ Toasty.warning(mContext, "相机授权失败" ).show() } Manifest.permission.WRITE_EXTERNAL_STORAGE->{ Toasty.warning(mContext, "写入存储授权失败" ).show() } Manifest.permission.READ_PHONE_STATE->{ Toasty.warning(mContext, "读取手机状态授权失败" ).show() } } } } } } }