最近在项目上因为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