- 正常权限不会直接给用户隐私权带来风险。如果您的应用在其清单中列出了正常权限,系统将自动授予该权限。
- 危险权限会授予应用访问用户机密数据的权限。如果您的应用在其清单中列出了正常权限,系统将自动授予该权限。如果您列出了危险权限,则用户必须明确批准您的应用使用这些权限。
权限组
所有危险的 Android 系统权限都属于权限组。如果设备运行的是 Android 6.0(API 级别 23),并且应用的 targetSdkVersion
是 23 或更高版本,则当用户请求危险权限时系统会发生以下行为:
- 如果应用请求其清单中列出的危险权限,而应用目前在权限组中没有任何权限,则系统会向用户显示一个对话框,描述应用要访问的权限组。对话框不描述该组内的具体权限。例如,如果应用请求
READ_CONTACTS
权限,系统对话框只说明该应用需要访问设备的联系信息。如果用户批准,系统将向应用授予其请求的权限。 - 如果应用请求其清单中列出的危险权限,而应用在同一权限组中已有另一项危险权限,则系统会立即授予该权限,而无需与用户进行任何交互。例如,如果某应用已经请求并且被授予了
READ_CONTACTS
权限,然后它又请求WRITE_CONTACTS
,系统将立即授予该权限。
任何权限都可属于一个权限组,包括正常权限和应用定义的权限。但权限组仅当权限危险时才影响用户体验。可以忽略正常权限的权限组。
- 如果设备运行的是 Android 5.1(API 级别 22)或更低版本,并且应用的
targetSdkVersion
是 22 或更低版本,则系统会在安装时要求用户授予权限。再次强调,系统只告诉用户应用需要的权限组,而不告知具体权限。 低于6.0的应用targetSdkVersion
是 22的版本运行时是强制用户允许在AndroidManifest.xml声明的权限的,而6.0以后,用户开始在应用运行时向其授予权限,而不是在应用安装时授予。
android { compileSdkVersion 23 buildToolsVersion "23.0.1" defaultConfig { applicationId "com.qinglin.permissiondemo" minSdkVersion 14 targetSdkVersion 23 versionCode 1 versionName "1.0" } }
<!--运行时权限 访问摄像头和本地存储-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<!-- 一般权限 访问网络-->
<uses-permission android:name="android.permission.INTERNET"/>
检查权限
如果您的应用需要危险权限,则每次执行需要这一权限的操作时您都必须检查自己是否具有该权限。用户始终可以自由调用此权限,因此,即使应用昨天使用了相机,它不能假设自己今天仍具有该权限。
要检查您是否具有某项权限,请调用 ContextCompat.checkSelfPermission()
方法。例如,以下代码段显示了如何检查 Activity 是否具有在日历中进行写入的权限:
// Assume thisActivity is the current activity int permissionCheck = ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_CALENDAR);
在某些情况下,您可能需要帮助用户了解您的应用为什么需要某项权限。例如,如果用户启动一个摄影应用,用户对应用要求使用相机的权限可能不会感到吃惊,但用户可能无法理解为什么此应用想要访问用户的位置或联系人。在请求权限之前,不妨为用户提供一个解释。请记住,您不需要通过解释来说服用户;如果您提供太多解释,用户可能发现应用令人失望并将其移除。
您可以采用的一个方法是仅在用户已拒绝某项权限请求时提供解释。如果用户继续尝试使用需要某项权限的功能,但继续拒绝权限请求,则可能表明用户不理解应用为什么需要此权限才能提供相关功能。对于这种情况,比较好的做法是显示解释。
为了帮助查找用户可能需要解释的情形,Android 提供了一个实用程序方法,即 shouldShowRequestPermissionRationale()
。如果应用之前请求过此权限但用户拒绝了请求,此方法将返回 true
。
注:如果用户在过去拒绝了权限请求,并在权限请求系统对话框中选择了 Don't ask again 选项,此方法将返回 false
。如果设备规范禁止应用具有该权限,此方法也会返回 false
。
2、请求您需要的权限
如果应用尚无所需的权限,则应用必须调用一个 requestPermissions()
方法,以请求适当的权限。应用将传递其所需的权限,以及您指定用于识别此权限请求的整型请求代码。此方法异步运行:它会立即返回,并且在用户响应对话框之后,系统会使用结果调用应用的回调方法,将应用传递的相同请求代码传递到 requestPermissions()
。
以下代码可以检查应用是否具备读取用户联系人的权限,并根据需要请求该权限:
// 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. } }
3、处理权限请求响应(onRequestPermissionsResult回调)
当应用请求权限时,系统将向用户显示一个对话框。当用户响应时,系统将调用应用的onRequestPermissionsResult()
方法,向其传递用户响应。您的应用必须替换该方法,以了解是否已获得相应权限。回调会将您传递的相同请求代码传递给requestPermissions()
。例如,如果应用请求READ_CONTACTS
访问权限,则它可能采用以下回调方法:
@Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case PluginUtil.BAIDU_READ_LOCATION_STATE: { // 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. Toast.makeText(this, "用户允许此权限", Toast.LENGTH_LONG).show(); } else { if(!ActivityCompat.shouldShowRequestPermissionRationale(this,permissions[0])){ //用户拒绝了此申请权限,并勾选了不再询问 ToastUtil.showToast(this,"点击权限,并打开全部权限"); PluginUtil.gotoAppDetailSettingIntent(this); return; } // permission denied, boo! Disable the // functionality that depends on this permission. Toast.makeText(this, "用户拒绝此权限", Toast.LENGTH_LONG).show(); } return; } // other 'case' lines to check for other // permissions this app might request }
}
系统显示的对话框说明了您的应用需要访问的权限组;它不会列出具体权限。例如,如果您请求 READ_CONTACTS
权限,系统对话框只显示您的应用需要访问设备的联系人。用户只需要为每个权限组授予一次权限。如果您的应用请求该组中的任何其他权限(已在您的应用清单中列出),系统将自动授予应用这些权限。当您请求此权限时,系统会调用您的 onRequestPermissionsResult()
回调方法,并传递 PERMISSION_GRANTED
,如果用户已通过系统对话框明确同意您的权限请求,系统将采用相同方式操作。
权限功能规范化(统一创建一个工具类PermissionUtils)
由于权限的功能会在多个地方反复使用,为了提高复用性,必然需要建立一个工具类,把常用方法写在这个工具类里面,方便调用。
以下是我的工具类封装PermissionUtils,共享给大家:
public class PermissionUtils { private static final String SCHEME = "package"; public static final int BAIDU_READ_LOCATION_STATE = 100; //自定义一个权限获取码,用于回调函数中做对应处理 public static void opendPermissionSetting(Context context) { String deviceInfo = getDeviceInfo(); LogUtil.i("hql", "deviceInfo:" + deviceInfo); if ("Xiaomi".equals(Build.MANUFACTURER)) { gotoMiuiPermission(context); } else if ("Meizu".equals(Build.MANUFACTURER)) { gotoMeizuPermission(context); } else if ("HUAWEI".equals(Build.MANUFACTURER)) { gotoHuaweiPermission(context); } else { if (Build.VERSION.SDK_INT >= 23) {// 用于判断是否为Android 6.0系统以上版本 gotoAppDetailSettingIntent(context); } else { gotoAppSettingIntent(context); } } } public static void needPermission(Activity context, int requestCode, String[] permissions) { try { // 申请一个(或多个)权限,并提供用于回调返回的获取码(用户定义) ActivityCompat.requestPermissions(context, permissions, requestCode); } catch (Exception e) { e.printStackTrace(); LogUtil.e("hql", e.toString()); } } // 判断权限集合是否都拥有,只要有一个缺少就返回false public static boolean isHasPermission(Context context, String... permissions) { for (String permission : permissions) { if (!isHasPermission(context, permission)) { return false; } } return true; } // 判断是否拥有权限 private static boolean isHasPermission(Context context, String permission) { Context mContext = context.getApplicationContext(); return ContextCompat.checkSelfPermission(mContext, permission) == PackageManager.PERMISSION_GRANTED; } /** * 跳转到miui的权限管理页面 */ public static void gotoMiuiPermission(Context context) { Intent i = new Intent("miui.intent.action.APP_PERM_EDITOR"); ComponentName componentName = new ComponentName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity"); i.setComponent(componentName); i.putExtra("extra_pkgname", context.getPackageName()); try { context.startActivity(i); } catch (Exception e) { e.printStackTrace(); gotoAppSettingIntent(context); } } /** * 跳转到魅族的权限管理系统 */ public static void gotoMeizuPermission(Context context) { Intent intent = new Intent("com.meizu.safe.security.SHOW_APPSEC"); intent.addCategory(Intent.CATEGORY_DEFAULT); intent.putExtra("packageName", BuildConfig.APPLICATION_ID); try { context.startActivity(intent); } catch (Exception e) { e.printStackTrace(); gotoAppSettingIntent(context); } } /** * 华为的权限管理页面 */ public static void gotoHuaweiPermission(Context context) { try { Intent intent = new Intent(); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); ComponentName comp = new ComponentName("com.huawei.systemmanager", "com.huawei.permissionmanager.ui.MainActivity");//华为权限管理 intent.setComponent(comp); context.startActivity(intent); } catch (Exception e) { e.printStackTrace(); gotoAppSettingIntent(context); } } /** * 打开应用详情页面intent */ public static void gotoAppDetailSettingIntent(Context context) { Intent localIntent = new Intent(); localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (Build.VERSION.SDK_INT >= 9) { localIntent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); localIntent.setData(Uri.fromParts(SCHEME, context.getPackageName(), null)); } else if (Build.VERSION.SDK_INT <= 8) { localIntent.setAction(Intent.ACTION_VIEW); localIntent.setClassName("com.android.settings", "com.android.settings.InstalledAppDetails"); localIntent.putExtra("com.android.settings.ApplicationPkgName", context.getPackageName()); } context.startActivity(localIntent); } /** * 打开系统设置界面 */ public static void gotoAppSettingIntent(Context context) { Intent intent = new Intent(Settings.ACTION_SETTINGS);//系统设置界面 context.startActivity(intent); } }
Android 6.0运行时权限勾选不再询问后该如何处理?
当第一次请求权限申请被拒绝后再进行第二次申请时,对话框中会多出一个 不再询问 的复选框。如果勾选了该复选框并且拒绝请求,那么以后将无法再申请该权限。也就是说在调用 requestPermissions() 后,onRequestPermissionsResult() 会立刻被调用并且申请结果为 PERMISSION_DENIED 。 其实这个时候还是有一根救命稻草的。1、 首先需要判断用户是否勾选了不再询问,使用方法:ActivityCompat.shouldShowRequestPermissionRationale(this,permissions[0])。2、 ActivityCompat 位于 support.v7 包中,因为运行时权限是 6.0 的新特性,使用该类可以省略对版本的判断当权限申请被拒绝并且shouldShowRequestPermissionRationale() 返回 false 就表示勾选了不再询问。转到设置界面现在我们唯一能做的就是跳转到我们 App 的设置界面,让用户手动开启权限了。关键代码如下:权限最佳做法
应用如果一味要求用户提供授权,可能会让用户无所适从。如果用户发现应用难以使用,或者担心应用会滥用其信息,他们可能不愿意使用该应用,甚至会将其完全卸载。以下最佳做法有助于避免此类糟糕的用户体验。考虑使用 intent
许多情况下,您可以使用以下两种方式之一来让您的应用执行某项任务。您可以将应用设置为要求提供权限才能执行操作。或者,您可以将应用设置为使用 intent,让其他应用来执行任务。
例如,假设应用需要使用设备相机才能够拍摄照片。应用可以请求
CAMERA
权限,以便允许其直接访问相机。然后,应用将使用 Camera API 控制相机并拍摄照片。利用此方法,您的应用能够完全控制摄影过程,并支持您将相机 UI 整合至应用中。不过,如果您无需此类完全控制,则可以使用
ACTION_IMAGE_CAPTURE
intent 来请求图像。发送该 intent 时,系统会提示用户选择相机应用(如果没有默认相机应用)。用户使用选定的相机应用拍摄照片,该相机应用会将照片返回给应用的onActivityResult()
方法。同样,如果您需要拨打电话、访问用户的联系人或要执行其他操作,可以通过创建适当的 intent 来完成,或者您也可以请求相应的权限并直接访问相应的对象。每种方法各有优缺点。
如果使用权限:
- 您的应用可在您执行操作时完全控制用户体验。不过,如此广泛的控制会增加任务的复杂性,因为您需要设计适当的 UI。
- 系统会在运行或安装应用时各提示用户提供一次权限(具体取决于用户的 Android 版本)。之后,应用即可执行操作,不再需要用户进行其他交互。不过,如果用户不授予权限(或稍后撤销权限),您的应用将根本无法执行操作。
如果使用 intent:
- 您无需为操作设计 UI。处理 intent 的应用将提供 UI。不过,这意味着您无法控制用户体验。用户可能与您从未见过的应用交互。
- 如果用户没有适用于操作的默认应用,则系统会提示用户选择一款应用。如果用户未指定默认处理程序,则他们每次执行此操作时都必须处理一个额外对话框。
- 仅要求您需要的权限
每次您要求权限时,实际上是在强迫用户作出决定。您应尽量减少提出这些请求的次数。如果用户运行的是 Android 6.0(API 级别 23)或更高版本,则每次用户尝试要求提供权限的新应用功能时,应用都必须中断用户的操作并发起权限请求。如果用户运行的是较早版本的 Android,则在安装应用时需要为应用的每一权限请求给予授权;如果列表过长或看起来不合适,用户可能会决定不安装该应用。为此,您应尽量减少应用需要的权限数。
例如,很多情况下应用可以通过使用 intent 来避免请求权限。如果某项功能并非应用的核心功能,不妨考虑将相关工作交给其他应用来执行,如考虑使用 intent 中所述。
不要让用户感到无所适从
如果用户运行的是 Android 6.0(API 级别 23)或更高版本,则用户必须在应用运行时为其授权。如果您的应用一次要求用户提供多项权限,用户可能会感到无所适从并因此退出应用。您应根据需要请求权限。
某些情况下,一项或多项权限可能是应用所必需的。在这种情况下,合理的做法是,在应用启动之后立即要求提供这些权限。例如,如果您运行摄影应用,应用需要访问设备的相机。在用户首次启动应用时,他们不会对提供相机使用权限的要求感到惊讶。但是,如果同一应用还具备与用户联系人共享照片的功能,您不应在应用首次启动时要求用户提供
READ_CONTACTS
权限,而应等到用户尝试使用“共享”功能之后,再要求提供该权限。如果应用提供了教程,则合理的做法是,在教程结束时请求提供应用的必要权限。
解释需要权限的原因:
系统在您调用
requestPermissions()
时显示的权限对话框将说明应用需要的权限,但不会解释为何需要这些权限。某些情况下,用户可能会感到困惑。因此,最好在调用requestPermissions()
之前向用户解释应用需要相应权限的原因。例如,摄影应用可能需要使用位置服务,以便能够为照片添加地理标签。通常,用户可能不了解照片能够包含位置信息,并且对摄影应用想要了解具体位置感到不解。因此在这种情况下,应用最好在调用
requestPermissions()
之前告知用户此功能的相关信息。告知用户的一种办法是将这些请求纳入应用教程。这样,教程可以依次显示应用的每项功能,并在显示每项功能时解释需要哪些相应的权限。例如,摄影应用的教程可以演示其“与您的联系人共享照片”功能,然后告知用户需要为应用授予权限才能查看用户的联系人。然后,应用可以调用
requestPermissions()
,要求用户提供该访问权限。当然,并非所有用户都会按照教程操作,因此您仍需在应用的正常操作期间检查和请求权限测试两种权限模式
从 Android 6.0(API 级别 23)开始,用户是在运行时而不是在应用安装时授予或撤销应用权限。因此,您应在多种不同条件下测试应用。在低于 Android 6.0 的版本中,您可以认为如果应用得到运行,它就可以得到在应用清单中声明的全部权限。在新的权限模式中,这一推断不再成立。
以下提示可帮助您识别在运行 API 级别 23 或更高级别的设备上与权限有关的代码问题:
- 识别应用的当前权限和相关的代码路径。
- 在各种受权限保护的服务和数据中测试用户流程。
- 使用授予或撤销权限的各种组合进行测试。例如,相机应用可能会在清单中列出
CAMERA
、READ_CONTACTS
和ACCESS_FINE_LOCATION
。您应在测试应用时逐一打开和关闭这些权限,确保应用可以妥善处理所有权限配置。请记住,自 Android 6.0 起,用户可以打开或关闭任何应用的权限,即使面向 API 级别 22 或更低级别的应用也是如此。- 使用 adb 工具从命令行管理权限:
- 按组列出权限和状态:
$ adb shell pm list permissions -d -g
- 授予或撤销一项或多项权限:
$ adb shell pm [grant|revoke] <permission-name> ...