android6.0以前,我们把app需要用到的权限全部罗列在Manifest清单文件中。安装app时android系统会询问用户是否授予这些权限,拒绝后则无法安装app。如果授予,则安装app,之后无法修改授予状态。
android6.0将权限分为普通权限(不涉及用户隐私和安全)和危险权限(设计用户隐私和安全)。普通权限和andorid6.0之前一样,在Manifest清单文件中申请即可。危险权限需要在使用时动态申请,由用户决定是否授予。并且危险权限被分成9组,如果应用在运行时请求权限并且被授予该权限,将属于同一权限组并且在清单中注册的其他权限也会自动授予应用。
权限组名 | 权限名称 |
---|---|
CALENDAR(日历) | READ_CALENDAR |
WRITE_CALENDAR | |
CAMERA(相机) | CAMERA |
CONTACTS(联系人) | READ_CONTACTS |
WRITE_CONTACTS | |
GET_ACCOUNTS | |
LOCATION(位置) | ACCESS_FINE_LOCATION |
ACCESS_COARSE_LOCATION | |
MICROPHONE(麦克风) | RECORD_AUDIO |
PHONE(手机) | READ_PHONE_STATE |
CALL_PHONE | |
ERAD_CALL_LOG | |
WRITE_CALL_LOG | |
ADD_VOICEMAIL | |
USE_SIP | |
PROCESS_OUTGOING_CALLS | |
SENSORS(传感器) | BODY_SENSORS |
SMS(短信) | SEND_SMS |
RECEIVE_SMS | |
READ_SMS | |
RECEIVE_WAP_PUSH | |
RECEIVE_MMS | |
STORAGE(存储卡) | READ_EXTERNAL_STORAGE |
WRITE_EXTERNAL_STORAGE |
以申请CALL_PHONE权限为例,动态申请权限的代码如下:
//检测是否具备该权限
boolean haveCallPhonePermission = checkCallPhonePermission();
if (!haveCallPhonePermission) { //不具备该权限
if (shouldShowUIToExplain()) { //需要弹出UI解释授予该权限的必要性
showExplainAndRequestCallPhonePermission(); //弹出UI解释权限用途并再次申请
} else {
requestPhonePermission(); //直接申请权限
}
}
为了直观,我把关键部分的代码都抽离成方法。
先看第一部分,检测是否具备某个权限,
public boolean checkCallPhonePermission() {
return ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED;
}
ContextCompat.checkSelfPermission()用于检测是否具备某个权限,返回值为PackageManager.PERMISSION_GRANTED时表示具备该权限,为PackageManager.PERMISSION_DENIED时表示不具备该权限。
再看第二部分,检测是否需要展示UI向用户解释权限用途以说服用户授予该权限,
public boolean shouldShowUIToExplain() {
return ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, android.Manifest.permission.CALL_PHONE);
}
ActivityCompat.shouldShowRequestPermissionRationale方法检测是否需要就该权限向用户说明授予权限的必要性。只有用户拒绝过该权限并且没有选择“拒绝后不再提示”时,该方法返回true,我们弹出UI解释授予权限的必要性,用户确认后再次申请该权限,
private void showExplainAndRequestCallPhonePermission() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("提示")
.setMessage("不授予打电话权限将无法通过本app直接拨打电话,降低您的体验!")
.setPositiveButton("授予", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPhonePermission();
}
}).create().show();
}
如果app没有申请过该权限(第一次申请前),用户不一定拒绝该权限,所以没有必要进行解释,ActivityCompat.shouldShowRequestPermissionRationale返回false;如果用户拒绝了该权限且选择不再提示,那么当我们申请该权限时,系统会直接拒绝,不会询问用户,这种情况下也返回false。
在ActivityCompat.shouldShowRequestPermissionRationale返回false的情况下,我们直接申请该权限,
private void requestPhonePermission() {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.CALL_PHONE}, REQUEST_CALL_PHONE_CODE);
}
ActivityCompat.requestPermissions方法用于申请权限,第二个参数为数组,表示可一次申请多个权限,第三个参数为请求码,标志该次请求(int值)。
你可能会疑惑,如果用户拒绝了该权限且选择不再提示时,我们直接申请权限有什么用呢?
这是因为app没有申请过该权限(第一次申请前)时,ActivityCompat.shouldShowRequestPermissionRationale也返回false,所以无法判断是否从未申请过,还是要申请权限。那如何获知用户拒绝了权限且选择了不再提示呢?我们可以在权限申请结果的回调中检测。
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (grantedCallPhonePermission(requestCode, permissions, grantResults)) { //用户授予了该权限
} else { //用户拒绝了该权限
if (!shouldShowUIToExplain()) { //用户拒绝了该权限并且选择了不再提示
showExplainAndToSettingPage();//向用户解释权限的必要性,并引导其到设置界面开启权限
}
}
}
在activity的onRequestPermissionsResult方法中会得到权限请求的回调。
先看如何判断权限是否被授予,
private boolean grantedCallPhonePermission(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
return requestCode == REQUEST_CALL_PHONE_CODE && permissions[0] == Manifest.permission.CALL_PHONE && grantResults[0] == PackageManager.PERMISSION_GRANTED;
}
判断请求码以确定该次回调是否为请求Call phone权限的结果回调。grantResults数组为结果列表,对应permissions数组的申请结果。
如果权限被拒绝,那么此时调用ActivityCompat.shouldShowRequestPermissionRationale()方法返回false的话,一定是用户选择了“不再提示”。那么我们可以弹出UI提示该权限的必要性并引导他去设置界面开启该权限。
private void showExplainAndToSettingPage() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("提示")
.setMessage("不授予打电话权限将无法通过本app直接拨打电话,降低您的体验!请到设置页面开启该权限。")
.setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
toSettingPage(); //挑传到设置界面
}
}).create().show();
}
private void toSettingPage(){
Intent intent = new Intent();
intent.setAction(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivityForResult(intent, REQUEST_TO_SETTING_CODE);
}
在acitivity的onActivityResult()方法里更新权限授予状态。
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_TO_SETTING_CODE && resultCode == RESULT_OK){
//这里可以重新检测是否授予了该权限
boolean haveGrantThePermission = checkCallPhonePermission();
}
}
至此,如何动态申请危险权限的代码就讲述完了。这里有个小问题要注意,上述申请权限的相关代码中我们并没有区分sdk版本是否6.0以上,因为我们使用的都是支持库中的代码,支持库内部会自动适配。
另外,由于国内厂商对android系统五花八门的定制,很多手机设备上,ActivityCompat.shouldShowRequestPermissionRationale()方法总是返回false,我们无法再通过该方法判断展示ui以解释权限及跳转到设置页面的时机。那么我们本地记录权限的申请次数,每次申请前先展示UI解释权限的作用,在用户确认我们的提示信息后,如果是第一次申请则直接申请该权限,否则引导该用户至设置界面开启权限。
自android6.0起,HttpClient系列代码从sdk中剔除,推荐我们使用HttpURLConnection。现在主流的网路请求框架是square公司出的okhttp/retrofit框架。
对于老项目,将编译版本升级至6.0以上时编译报错,找不到HttpClient相关代码。可是网络请求代码遍布整个项目,不太可能把HttpClient相关代码一下子全部拿掉,因为项目还要继续开发新需求并发布新版本,需要HttpClient和新引入的网络框架共存一段时间,直至将HttpClient相关代码全部重构掉。我们可以在module的gradle文件里如下配置,
android {
useLibrary 'org.apache.http.legacy'
...
}
如此,就可以继续使用HttpClient了,待HttpClient相关代码全部重构完毕,再去掉该配置。