权限机制
对于6.0以下的权限在安装的时候,根据权限声明产生一个权限列表,用户只有在同意之后才能完成app的安装,造成了我们想要使用某个app,就要忍受其一些不必要的权限。
Android 6.0推出了新的权限机制,我们可以直接安装,当app需要我们授予不恰当的权限的时候,我们可以予以拒绝。当然我们也可以在设置界面对每个app的权限进行查看,以及对单个权限进行授权或者解除授权。
Google将权限分为两类,一类是Normal Permissions,这类权限一般不涉及用户隐私,是不需要用户进行授权的,比如手机震动、访问网络等;另一类是Dangerous Permission,一般是涉及到用户隐私的,需要用户进行授权,比如读取sdcard、访问通讯录等,新的权限机制更好的保护了用户的隐私。
Normal Permission:
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_INSTALL_PACKAGES
SET_ALARM
SET_TIME_ZONE
SET_WALLPAPER
SET_WALLPAPER_HINTS
TRANSMIT_IR
UNINSTALL_SHORTCUT
USE_FINGERPRINT
VIBRATE
WAKE_LOCK
WRITE_SYNC_SETTINGS
我们可以用 adb 命令查看危险的权限,Dangerous Permission:
adb shell pm list permissions -d -g
Dangerous Permissions:
group:android.permission-group.CONTACTS
permission:android.permission.WRITE_CONTACTS
permission:android.permission.GET_ACCOUNTS
permission:android.permission.READ_CONTACTS
group:android.permission-group.PHONE
permission:android.permission.READ_CALL_LOG
permission:android.permission.READ_PHONE_STATE
permission:android.permission.CALL_PHONE
permission:android.permission.WRITE_CALL_LOG
permission:android.permission.USE_SIP
permission:android.permission.PROCESS_OUTGOING_CALLS
permission:com.android.voicemail.permission.ADD_VOICE
group:android.permission-group.CALENDAR
permission:android.permission.READ_CALENDAR
permission:android.permission.WRITE_CALENDAR
group:android.permission-group.CAMERA
permission:android.permission.CAMERA
group:android.permission-group.SENSORS
permission:android.permission.BODY_SENSORS
group:android.permission-group.SUPERUSER
permission:android.permission.ACCESS_SUPERUSER
group:android.permission-group.LOCATION
permission:android.permission.ACCESS_FINE_LOCATION
permission:android.permission.ACCESS_COARSE_LOCATION
group:android.permission-group.STORAGE
permission:android.permission.READ_EXTERNAL_STORAGE
permission:android.permission.WRITE_EXTERNAL_STORAGE
group:android.permission-group.MICROPHONE
permission:android.permission.RECORD_AUDIO
group:android.permission-group.SMS
permission:android.permission.READ_SMS
permission:android.permission.RECEIVE_WAP_PUSH
permission:android.permission.RECEIVE_MMS
permission:android.permission.RECEIVE_SMS
permission:android.permission.SEND_SMS
permission:android.permission.READ_CELL_BROADCASTS
ungrouped:
看到上面的dangerous permissions,我们会发现危险权限都是以组来分的。那么分组会有什么影响。
如果app运行在Android 6.0 或以上的机器上,对于授权机制是这样的。
如果你申请某个危险的权限,假设你的app早已被用户授权了同一组的某个危险权限,那么系统会立即授权,而不需要用户去点击授权。比如你的app对READ_CONTACTS已经授权了,当你的app申请WRITE_CONTACTS时,系统会直接授权通过。
此外,对于申请时弹出的dialog上面的文本说明也是对整个权限组的说明,而不是单个权限(这个dialog是不能进行定制的)。
不过需要注意的是,不要对权限组过多的依赖,尽可能对每个危险权限都进行正常流程的申请,因为在后期的版本中这个权限组可能会产生变化。
API处理
新增的 API 有如下几个:
ContextCompat.checkSelfPermission
ActivityCompat.requestPermissions()
onRequestPermissionsResult()
ActivityCompat.shouldShowRequestPermissionRationale
checkSelfPermission 是检查我们是否已申请了某个权限,如果没有申请就调用 requestPermissions() 来进行权限申请。另外因为 ActivityCompat 继承了 ContextCompat,所以我们都用 ActivityCompat 也是可以的。
因为申请权限是个异步的过程,所以我们要在一个回调里去处理结果,就是 onRequestPermissionsResult(),我们在这里拿到用户是否通过了授权,通过就去做通过的处理,失败就做提示处理。
shouldShowRequestPermissionRationale 是告诉用户这个权限是干什么的,只有拒绝授予权限之后再弹出权限框才会出现,你需要给用户一个解释。
使用流程:
在AndroidManifest文件中添加需要的权限。
这个步骤和我们之前的开发并没有什么变化,试图去申请一个没有声明的权限可能会导致程序崩溃。
检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { }else{ // }
checkSelfPermission 方法返回值为PackageManager.PERMISSION_DENIED或者PackageManager.PERMISSION_GRANTED。当返回DENIED就需要进行申请授权了。
权限申请
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, MY_PERMISSIONS_REQUEST_READ_CONTACTS); }else{ // }
该方法是异步的,第一个参数是Context;第二个参数是需要申请的权限的字符串数组;第三个参数为requestCode,主要用于回调的时候检测。可以从方法名requestPermissions以及第二个参数看出,是支持一次性申请多个权限的,系统会通过对话框逐一询问用户是否授权。
权限申请回调
@Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case MY_PERMISSIONS_REQUEST_READ_CONTACTS: { // 如果请求被拒绝了,这个结果数组是空的 if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 同意了,做你需要做的与该权限相关的任务。 } else { //许可拒绝,禁用依赖于此权限的功能。 } return; } } }
对于权限的申请结果,首先验证 requestCode 定位到我们的申请,然后验证 grantResults 对应于申请的结果,这里的数组对应于申请时的第二个权限字符串数组。如果我们同时申请两个权限,那么 grantResults 的 length 就为2,分别记录我们两个权限的申请结果。如果申请成功,就可以做我们的事情了。
步骤就是这样了,为了用户体验,我们还要把 shouldShowRequestPermissionRationale 向用户解释加进去。
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// 我们是否应该显示解释
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS)) {
// 给个解释给用户,不异步阻塞这个线程等待用户的响应!用户看到说明后,再试一次请求权限。
} else {
// 不需要解释,我们可以请求许可。
ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);
// MY_PERMISSIONS_REQUEST_READ_CONTACTS 是一个我们定义的int常数。回调方法获取请求的结果,我们可以给它赋值为0为1都可以。
}
}
封装
虽然权限申请并不复杂,但是重复的代码太多,我们对权限的操作并不是这个我们要开发 app 的要点,如果把它混杂在我们的 MainActivity 里会影响代码的可读性。所以在对权限的操作比较多的时候,我们最好对其进行封装。
原本我们的申请代码是这样的:
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.CALL_PHONE)
!= PackageManager.PERMISSION_GRANTED)
{
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CALL_PHONE},
MY_PERMISSIONS_REQUEST_CALL_PHONE);
} else
{
//
}
可以看到不同权限的逻辑是相同的,不同的只是参数。通过上面的代码,我们可以知道需要的是这三个参数:
- Activity | Fragment
- permissions(权限字符串数组)
- requestCode(int型申请码)
所以我们只要写个方法来接收这三个参数即可,所有权限逻辑都相同。通常我们都会把公共的方法都写在一个 BaseActivity 里,然后让我们的 MainActivity 去继承它,在这里我也这样去做。
BaseActivity.java:
public class BaseActivity extends AppCompatActivity {
protected void isPermissionGranted(String permission, int requestCode) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
return ;
}
//判断是否需要请求允许权限
if (hasPermission(new String[]{permission})) {
requestPermissions(new String[]{permission}, requestCode);
}
}
protected void isPermissionAllGranted(String[] permissions, int requestCode) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
return ;
}
//获得批量请求但被禁止的权限列表
List<String> deniedPerms = new ArrayList<String>();
for(int i = 0; permissions != null && i < permissions.length;i++){
if (!hasPermission(new String[]{permissions[i]})) {
deniedPerms.add(permissions[i]);
}
}
//进行批量请求
int denyPermNum = deniedPerms.size();
if(denyPermNum != 0){
requestPermissions(deniedPerms.toArray(new String[denyPermNum]), requestCode);
}
}
/**
* 为子类提供一个权限检查方法
* @param permissions
* @return
*/
public boolean hasPermission(String[] permissions) {
for (String permission: permissions) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
/**
* 为子类提供一个权限请求方法
* @param requestCode
* @param permissions
*/
public void requestPermission(int requestCode, String[] permissions) {
ActivityCompat.requestPermissions(this, permissions, requestCode);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if(grantResults.length == 0){
return;
}
switch (requestCode) {
case 1 :
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "未授予拨打电话权限", Toast.LENGTH_SHORT).show();
}else{
doCallPhone();
}
break;
case 2 :
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "未授予读取SD卡权限", Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(this, "用户已经授予读取SD卡权限", Toast.LENGTH_SHORT).show();
}
break;
case 3 :
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "有权限未授予,部分功能不能使用", Toast.LENGTH_SHORT).show();
break;
} else if (i == grantResults.length - 1){
Toast.makeText(this, "用户已经授予全部权限", Toast.LENGTH_SHORT).show();
}
}
break;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
private void doCallPhone() {
Intent intent = new Intent(Intent.ACTION_CALL);
Uri data = Uri.parse("tel:" + "10086");
intent.setData(data);
startActivity(intent);
}
}
MainActivity.java:
public class MainActivity extends BaseActivity {
private Button mButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
isPermissionAllGranted(new String[]{Manifest.permission.CALL_PHONE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 3);
mButton = (Button) findViewById(R.id.setting);
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//前往应用详情界面
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);
startActivity(intent);
} catch (Exception e) {
}
}
});
}
}
这里我做了一个小例子,申请了读取SD卡和拨打电话两个权限,可做批量申请,并且回调方法可进入拨号界面,如果 API < 23就不做申请,因为是6.0以下。因为这是举例,我没有创建常量类,拨打电话的 requestCode 是1,SD卡是2,批量是3。代码很简单,稍注意的是在批量申请时有 List 集合记录未申请的权限。
界面上有一个 Button,点击可进入系统的设置,可手动更改权限。
如果是 Fragment,我们只要用getActivity()方法获得 Activity 对象,通过这个对象调用checkSelfPermission相关方法就可以啦。
public Activity getActivity(Object object) {
if (object instanceof Activity) {
return (Activity) object;
} else if (object instanceof Fragment) {
return ((Fragment) object).getActivity();
}
return null;
}
结束语:本文仅用来学习记录,参考查阅。