从 Android 6.0(API 级别 23)开始,用户开始在应用运行时向其授予权限,而不是在应用安装时授予。 Android 6.0系统6.0以前,所有的权限,访问网络的权限,读取SD卡的权限,访问通讯录,拨打电话的权限都在安装的时候系统授予了要安装的应用。在系统运行的时候不需要对权限做任何的处理。用户在安装的时候一般都不知道这些权限用在了什么地方,这样做很不安全。一个应用很容易在一个Service中读取用户所有的通讯录信息发送到服务器上。Android 6.0后的动态权限让我们的系统更加安全,牺牲了用户的方便性,得到的是安全。下图是Android 6.0系统对拨打电话和管理电话权限组的询问。
Android 6.0把权限分为两种:Normal Permissions(正常权限)和Dangerous Permissions(危险权限)。其中危险权限又进行了分类。把所有的危险权限分成了几个组。正常权限不会给用户的隐私带来不安全,不需要动态申请,在应用安装的时候就已经被授予了。危险权限需要动态处理,只有用户批准了这些权限,应用才能被授予这些权限。
所有的普通权限:
- ACCESS_LOCATION_EXTRA_COMMANDS
- ACCESS_NETWORK_STATE
- ACCESS_NOTIFICATION_POLICY
- ACCESS_WIFI_STATE
- BLUETOOTH
- BLUETOOTH_ADMIN
- BROADCAST_STICKY
- CHANGE_NETWORK_STATE
- CHANGE_WIFI_MULTICAST_STATE
- CHANGE_WIFI_STATE
- DISABLE_KEYGUARD
- EXPAND_STATUS_BAR
- GET_PACKAGE_SIZE
- INSTALL_SHORTCUT
- INTERNET
- KILL_BACKGROUND_PROCESSES
- MODIFY_AUDIO_SETTINGS
- NFC
- READ_SYNC_SETTINGS
- READ_SYNC_STATS
- RECEIVE_BOOT_COMPLETED
- REORDER_TASKS
- REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
- REQUEST_INSTALL_PACKAGES
- SET_ALARM
- SET_TIME_ZONE
- SET_WALLPAPER
- SET_WALLPAPER_HINTS
- TRANSMIT_IR
- UNINSTALL_SHORTCUT
- USE_FINGERPRINT
- VIBRATE
- WAKE_LOCK
- WRITE_SYNC_SETTINGS
所有的危险权限:
二、Android 6.0+危险权限的动态处理
在开发应用的时候不管是正常权限还是危险权限都必须在应用的Manifest.xml文件中声明。如果设备运行的是Android 5.1或更低的系统,或者应用的目标SDK小于23,那么在Manifest.xml文件中列出的危险权限在安装的时候用户必须接受,要不应用没法安装。如果设备运行的是Android 6.0或更高的系统,或者应用的目标SDK大于等于23,那么在Manifest.xml文件中列出的危险权限,会在应用运行的时候被用户授予或拒绝。开发者需要在代码中对危险权限进行处理。
public class MainActivity extends AppCompatActivity {
final public static int REQUEST_CODE_ASK_CALL_PHONE = 123;
private String mMobile;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.button).setOnClickListener(new
View.OnClickListener() {
@Override
public void onClick(View v) {
onCallWrapper("12345678912");
}
});
}
public void onCallWrapper(String mobile) {
this.mMobile = mobile;
//检测权限
int checkCallPhonePermisssion = ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE);
if (checkCallPhonePermisssion != PackageManager.PERMISSION_GRANTED || !checkOpsPermission(this,Manifest.permission.CALL_PHONE)) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CALL_PHONE)) {
Toast.makeText(this, "shouldShowRequestPermissionRationale", Toast.LENGTH_SHORT).show();
new AlertDialog.Builder(this)
.setTitle("提示")
.setMessage("应用需要开启拍照的权限,是否继续?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE}, REQUEST_CODE_ASK_CALL_PHONE);
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
}).show();
} else {
//请求权限
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.CALL_PHONE}, REQUEST_CODE_ASK_CALL_PHONE);
}
} else {
callDirectly(mobile);
}
}
//权限检测
private static boolean checkOpsPermission(Context context, String permission) {
//6.0 api 23
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
try {
AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
String opsName = null;
opsName = AppOpsManager.permissionToOp(permission);
if (opsName == null) {
return true;
}
int opsMode = appOpsManager.checkOpNoThrow(opsName, Process.myUid(), context.getPackageName());
return opsMode == AppOpsManager.MODE_ALLOWED;
} catch (Exception ex) {
return true;
}
}
return true;
}
//请求权限返回的结果
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ASK_CALL_PHONE:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission Granted 授予权限
callDirectly(mMobile);
} else {
// Permission Denied 权限被拒绝
Toast.makeText(MainActivity.this, "Permission Denied",
Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void callDirectly(String mobile) {
Intent intent = new Intent();
intent.setAction("android.intent.action.CALL");
intent.setData(Uri.parse("tel:" + mobile));
this.startActivity(intent);
}
}
上面这个简单的例子是通过点击一个按钮拨打电话。在6.0系统之前,我们只需要直接调用方法callDirectly()方法就可以了。增加了系统的安全性,但是多了很多代码上的处理。ContextCompat和
ActivityCompat分别是Context和Activity的兼容类,在上的代码中我们就不用去判断当前系统的API是多好了,在API23之前也能用上面代码中的方法。ContextCompat.checkSelfPermission()方法是用来判断,当前应用是否拥有某个权限。如果拥有我们可以直接调用callDirectly()方法拨打电话。如果当前应用没有拨打电话的权限,会调ActivityCompat.shouldShowRequestPermissionRationale()方法,注意当前系统还没有弹出让用户去选择是允许还是拒绝,这个时候shouldShowRequestPermissionRationale()方法返回false。所以接下来会执ActivityCompat.requestPermissions()方法会向系统去请求,系统会弹出一个对话框让用户去选择。onRequestPermissionsResult()方法用来处理用户的选择。这个方法可以监听用户选择的是允许还是拒绝。当用户选择了允许,那么直接调用callDirectly()方法拨打电话。当用户选择了拒绝,上面的代码只是提示一个Toast。
当用户选择了拒绝,那么在下次点击按钮拨打电话的时候ActivityCompat.shouldShowRequestPermissionRationale()方法会返回true,在上面的代码中我们会弹出一个对话框给用户,给用户一个提示。当用户选择确定,会向系统请求权限。当用户选择取消,关闭对话框什么也不做。当我们选择了不在提示并且选择了拒绝的时候ActivityCompat.shouldShowRequestPermissionRationale()方法返回false。
三、用Easy Permissions开源库处理权限
1.配置
在app层的build.gradle中
dependencies {
// EasyPermissions
compile 'pub.devrel:easypermissions:1.2.0'
}
github : https://github.com/googlesamples/easypermissions
2.举例
EasyPermissions是谷歌封装的一个运行时权限申请的库,简化了操作的过程
1、builde gradle中依赖
2、清单文件中声明权限
3、重写onRequestPermissionsResult()方法,把执行操作给easyPermissions来
4、通过hasPermissions检查权限,或者原生的也行,然后去申请权限
5、实现EasyPermissions.PermissionCallbacks接口,重写两个方法,成功或失败
6、在成功或者失败方法中编写要具体做的事。
public class SecondActivity extends AppCompatActivity implements EasyPermissions.PermissionCallbacks {
private static final int RC_CAMERA_PERM = 123;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
cameraTask();
}
});
}
//RC_CAMERA_PERM 标识请求码, (可选)@AfterPermissionGranted()注解 .这里的方法名可以自己取,
//主要是权限都申请完,就调用这个方法,执行里面的操作。其实就相当于在onPermissionsGranted()调用这个方法而已:
@AfterPermissionGranted(RC_CAMERA_PERM)
public void cameraTask() {
//通过hasPermissions检查权限
if (EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA)) {
// 该应用已经有打电话的权限
Toast.makeText(this, "TODO: Camera things", Toast.LENGTH_LONG).show();
} else {
//请求权限
EasyPermissions.requestPermissions(this, "需要获取系统的拍照的权限!", RC_CAMERA_PERM, Manifest.permission.CAMERA);
}
}
//重新以下三个方法 1,把执行结果的操作给EasyPermissions
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
//请求成功执行相应的操作
@Override
public void onPermissionsGranted(int requestCode, List perms) {
Toast.makeText(this, "onPermissionsGranted", Toast.LENGTH_SHORT).show();
}
//申请失败时调用
@Override
public void onPermissionsDenied(int requestCode, List perms) {
Toast.makeText(this, "onPermissionsDenied", Toast.LENGTH_SHORT).show();
//不一定执行,有些手机会执行系统的弹框
new AppSettingsDialog.Builder(this)
.setTitle("权限申请")
.setPositiveButton("确认")
.setNegativeButton("取消")
.setRationale("当前App需要申请camera权限,需要打开设置页面么?")
.setRequestCode(RC_CAMERA_PERM)
.build()
.show();
}
}
也可以通过原生来检查权限,比如有多个权限,可以抽取一个工具类
public final class CheckPermissionUtils {
private CheckPermissionUtils() {
}
//需要申请的权限
private static String[] permissions = new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.CAMERA
};
//检测权限
public static String[] checkPermission(Context context){
List data = new ArrayList<>();//存储未申请的权限
for (String permission : permissions) {
int checkSelfPermission = ContextCompat.checkSelfPermission(context, permission);
if(checkSelfPermission == PackageManager.PERMISSION_DENIED){//未申请
data.add(permission);
}
}
return data.toArray(new String[data.size()]);
}
}
以下是在activity中调用该工具类
//初始化权限
private void initPermission() {
//检查权限
String[] permissions = CheckPermissionUtils.checkPermission(this);
if (permissions.length == 0) {
//权限都申请了
} else {
//申请权限 ,参数2:权限数组,参数3:请求码code
ActivityCompat.requestPermissions(this, permissions, 100);
//把执行结果的操作给EasyPermissions的onRequestPermissionsResult
}
}
这个例子是点击一个按钮,处理拍照的事情,所以我们需要获取相机的权限。首先我们要复写Activity或者Fragment的onRequestPermissionsResult()方法。SecondActivity实现了EasyPermissions.PermissionCallbacks这个接口复写了onPermissionsGranted()和onPermissionsDenied()这两个接口。当用户选择了允许那么调用onPermissionsGranted()方法,当用户选择了拒绝那么调用onPermissionsDenied()方法。AppSettingsDialog是一个询问用户是否跳转到运行应用的设置界面去开启权限的对话框。
四、用Permissions Dispatcher开源库处理权限
1.配置
在app层的build.gradle中
dependencies {
// Permissions Dispatcher
compile 'com.github.hotchemi:permissionsdispatcher:3.0.1'
annotationProcessor 'com.github.hotchemi:permissionsdispatcher-processor:3.0.1'
}
这种配置需要有个条件:Android Gradle Plugin >= 2.2
2.举例
这个库用到了5个注解:
注解 | 是否必须 | 描述 |
---|---|---|
@RuntimePermissions | 是 | 注册在Acttivity或者Fragment上 |
@NeedsPermission | 是 | 再需要权限的方法上注册 |
@OnShowRationale | 否 | 被注解的方法可以提示为什么需要这个权限 |
@OnPermissionDenied | 否 | 如果用户拒绝了权限申请那么调用该方法 |
@OnNeverAskAgain | 否 | 如果用户选择了不再询问,调用该方法 |
这个库用到了Annotion Processor,需要用到一个应用编译期间产生的类,这个类的命名是:类名+PermissionsDispatcher。
//注册在Acttivity或者Fragment上
@RuntimePermissions
public class ThirdActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_third);
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ThirdActivityPermissionsDispatcher.showContactsWithCheck(ThirdActivity.this);
}
});
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
ThirdActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
}
//处理当用户允许该权限时需要处理的方法
@NeedsPermission({Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS})
public void showContacts() {
Toast.makeText(this, "读取通讯录", Toast.LENGTH_SHORT).show();
}
//如果用户拒绝了权限申请那么调用该方法
@OnPermissionDenied({Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS})
public void onContactsDenied() {
Toast.makeText(this, "onContactsDenied", Toast.LENGTH_SHORT).show();
}
//如果用户选择了不再询问,调用该方法
@OnNeverAskAgain({Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS})
public void onContactsNeverAskAgain() {
Toast.makeText(this, "onContactsNeverAskAgain", Toast.LENGTH_SHORT).show();
}
//// 提示用户权限使用的对话框
@OnShowRationale({Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS})
public void showRationaleContacts(final PermissionRequest request) {
// Toast.makeText(this, "showRationaleContacts", Toast.LENGTH_SHORT).show();
new AlertDialog.Builder(this)
.setPositiveButton("继续", new DialogInterface.OnClickListener() {
@Override
public void onClick(@NonNull DialogInterface dialog, int which) {
request.proceed();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(@NonNull DialogInterface dialog, int which) {
request.cancel();
}
})
.setCancelable(false)
.setMessage("需要通讯录权限")
.show();
}
}
其实这个dispather就是对动态权限处理的那些操作进行了封装,通过查看生成的MainActivityPermissionsDispatcher文件就可以验证这一点。这里的权限说的是危险权限,普通权限只用在Manifest中声明就可以了。
五、一句代码搞定权限请求,XXPermissions
github : https://github.com/getActivity/XXPermissions
集成步骤
dependencies {
implementation 'com.hjq:xxpermissions:3.2'
}
使用
XXPermissions.with(this)
//.constantRequest() //可设置被拒绝后继续申请,直到用户授权或者永久拒绝
//.permission(Permission.REQUEST_INSTALL_PACKAGES, Permission.SYSTEM_ALERT_WINDOW) //支持请求安装权限和悬浮窗权限
.permission(Permission.Group.STORAGE) //支持多个权限组进行请求,不指定则默以清单文件中的危险权限进行请求
.request(new OnPermission() {
@Override
public void hasPermission(List granted, boolean isAll) {
if (isAll) {
Toast.makeText(MainActivity.this, "获取权限成功", Toast.LENGTH_SHORT).show();
}else {
Toast.makeText(MainActivity.this, "获取权限成功,部分权限未正常授予", Toast.LENGTH_SHORT).show();
}
}
@Override
public void noPermission(List denied, boolean quick) {
if(quick) {
Toast.makeText(MainActivity.this, "被永久拒绝授权,请手动授予权限", Toast.LENGTH_SHORT).show();
//如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.gotoPermissionSettings(MainActivity.this);
}else {
Toast.makeText(MainActivity.this, "获取权限失败", Toast.LENGTH_SHORT).show();
}
}
});
参考文献:
https://github.com/googlesamples/easypermissions
https://github.com/hotchemi/PermissionsDispatcher
http://hotchemi.github.io/PermissionsDispatcher/
https://inthecheesefactory.com/blog/things-you-need-to-know-about-android-m-permission-developer-edition/en
https://developer.android.com/guide/topics/security/permissions.html#normal-dangerous
系统所需要的权限
https://developer.android.com/reference/android/Manifest.permission.html
普通权限
https://developer.android.com/guide/topics/permissions/normal-permissions.html
危险权限
https://developer.android.com/guide/topics/security/permissions.html#perm-groups
在运行时请求权限
https://developer.android.com/training/permissions/requesting.html
解读Android官方开发指导 - 运行时权限
http://www.jianshu.com/p/0beb6243d650