一、概述
在Android6.0之前的系统中,APP只要在AndroidManifest.xml声明了权限,就获得了授权,用户只能选择授权或者不安装该应用。Android 6.0在原有的AndroidManifest.xml声明权限的基础上,又新增了运行时权限动态检测,使用:日历、摄像头、通讯录、地理位置、麦克风、电话、短信、存储空间、身体传感器等权限都需要在运行时判断,使用其他权还是跟原来一样,只要在AndroidManifest.xml声明即获得授权。对于未获得相应权限授权的操作,将返回空或者抛出异常,如果APP中没有对此作出良好的处理,有可能产生崩溃现象。因此,在Android 6.0中开发APP,我们需要编写代码适应这种机制。
二、运行时权限管理
Android系统中的权限被分为以下几类:
1)normal:表示权限是低风险的,不会对系统、用户或其他应用程序造成危害,应用直接授予此权限;
2)dangerous:表示权限是高风险的,系统将可能要求用户输入相关信息,才会授予此权限;
3)signature:表示只有当应用程序所用数字签名与声明引权限的应用程序所用数字签名相同时,才能将权限授给它;
4)signatureOrSystem: 表示将权限授给具有相同数字签名的应用程序或system分区的应用,已废弃,使用privileged代替。
5)privileged:同signatureOrSystem。
在Android 6.0以前的系统中,“dangerous”与“normal”等级的权限是没有区别的,只要在AndroidManifest.xml声明了权限,就授予授权。在Android 6.0及以后系统中,25个“dangerous”等级的权限被分为9组,需要由应用程序自己去请求授权,不请求的话默认是“禁止”,同一组的任何一个权限被授权了,其他权限也自动被授权。
Permission Group | Permissions |
---|---|
android.permission-group.CALENDAR |
|
android.permission-group.CAMERA |
|
android.permission-group.CONTACTS |
|
android.permission-group.LOCATION |
|
android.permission-group.MICROPHONE |
|
android.permission-group.PHONE |
|
android.permission-group.SENSORS |
|
android.permission-group.SMS |
|
android.permission-group.STORAGE |
|
权限请求会触发系统弹出权限请求对话框。如果是第一次请求该权限,则会有权限请求对话框弹出来,如下图所示:
如果第一次用户选择了“拒绝”,那么第二次请求权限弹出的权限请求对话框弹出来会包含一个“不再询问”勾选框,如下图所示:
如果用户选择了“允许”,无论是否勾选”记住“,以后每次权限请求都会允许,不会再弹出权限请求对话框(实测结果是这样,貌似此处Google逻辑有点问题)。如果用户没有勾选“记住”,且用户选择“拒绝”,那么每次权限请求都会触发上图的弹出权限请求对话框,除非用户选择“允许”或者勾选“记住”。
用户可以进入“设置”-“应用”中去修改应用的权限授权状态,在此处修改后,应用程序权限请求时将不再弹出权限请求对话框。
由于目前绝大部分应用还不支持这种特性,因此Google做了妥协,targetSdkVersion<23时,采用以前的权限管理方式,AndroidManifest.xml中声明的权限还是安装后全部授权,当APP的targetSdkVersion大于等于23时,APP中的一些权限将需要APP自己添加代码申请权限。
三、相关API
Android 6.0 中增加了一些API用于运行时权限的申请与授权状态查询:
1)Context
Context中提供了查询当前应用权限授权状态的API——checkSelfPermission, 当其返回0,表示已授权(PackageManager.PERMISSION_GRANTED);返回1,表示未授权(PackageManager.PERMISSION_DENIED):
public int checkSelfPermission(String permission) { if (permission == null) { throw new IllegalArgumentException("permission is null"); } return checkPermission(permission, Process.myPid(), Process.myUid()); }
2) Activity
在Activity中提供了判断是否应该说明申请权限理由的API——shouldShowRequestPermissionRationale,该函数返回值如下:
a. 当应用第一次请求权限时返回false;
b. 第一次请求权限时,用户拒绝了,下一次返回 true,这时可以给用户说明需要改权限的理由;
c. 第二次请求权限时,用户拒绝了,并选择了“不再提醒”的选项时,返回false。以后权限申请对话框是不会弹出的,权限请求都会被拒绝。
d. 在请求权限时,用户允许了,下一次返回false;
e. 第二次以后请求权限时,用户拒绝了,并没有选择“不再提醒”的选项时,返回true,这是也可以给用户说明需要改权限的理由;
public boolean shouldShowRequestPermissionRationale(@NonNull String permission) { return getPackageManager().shouldShowRequestPermissionRationale(permission); }
另外提供了申请权限的API——requestPermissions,它实质上是一个startActivityForResult,里面可以请求多个权限,会启动一个系统Activity来判断该应用的权限状态。没有永久禁止或者允许的话会弹出权限请求对话框
public final void requestPermissions(@NonNull String[] permissions, int requestCode) { if (mHasCurrentPermissionsRequest) { Log.w(TAG, "Can reqeust only one set of permissions at a time"); // Dispatch the callback with empty arrays which means a cancellation. onRequestPermissionsResult(requestCode, new String[0], new int[0]); return; } Intent intent = getPackageManager().buildRequestPermissionsIntent(permissions); startActivityForResult(REQUEST_PERMISSIONS_WHO_PREFIX, intent, requestCode, null); mHasCurrentPermissionsRequest = true; }
requestPermissions返回的结果在onRequestPermissionsResult,传入参数包含权限是否授权的信息。
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { /* callback - no nothing */ }
3)Fragement
Fragement 中提供的API与Activity类似,也提供shouldShowRequestPermissionRationale、requestPermissions、onRequestPermissionsResult等方法。
为了兼容旧版本的Android系统,Google在support-v4库中添加了类似的接口:
ContextCompat.checkSelfPermission()
ActivityCompat.requestPermissions()
ActivityCompat.OnRequestPermissionsResultCallback
ActivityCompat.shouldShowRequestPermissionRationale()
FragmentCompat.requestPermissions()
FragmentCompat.OnRequestPermissionsResultCallback
FragmentCompat.shouldShowRequestPermissionRationale()
四、应用适配
当应用的targetSdkVersion》=23时,应用中需要使用“dangerous”等级的权限时,都需要进行权限授权状态判断、申请权限、处理权限申请结果。可以按照以下流程进行:
如果APP是作为第三方APP,建议使用support-v4库提供的接口。
1)检测权限,例如:
int permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
2)请求权限,例如
// Check if the Camera permission has been granted if (permission== PackageManager.PERMISSION_GRANTED) { // Permission is already available, start camera preview Snackbar.make(mLayout, "Camera permission is available. Starting preview.", Snackbar.LENGTH_SHORT).show(); startCamera(); } else { // Permission is missing and must be requested. requestCameraPermission(); } private void requestCameraPermission() { // Permission has not been granted and must be requested. if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) { // Provide an additional rationale to the user if the permission was not granted // and the user would benefit from additional context for the use of the permission. // Display a SnackBar with a button to request the missing permission. Snackbar.make(mLayout, "Camera access is required to display the camera preview.", Snackbar.LENGTH_INDEFINITE).setAction("OK", new View.OnClickListener() { @Override public void onClick(View view) { // Request the permission ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA); } }).show(); } else { Snackbar.make(mLayout, "Permission is not available. Requesting camera permission.", Snackbar.LENGTH_SHORT).show(); // Request the permission. The result will be received in onRequestPermissionResult(). ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, PERMISSION_REQUEST_CAMERA); }
请求权限时可以一次性申请多个权限。
3)处理权限请求结果
@Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { // BEGIN_INCLUDE(onRequestPermissionsResult) if (requestCode == PERMISSION_REQUEST_CAMERA) { // Request for camera permission. if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Permission has been granted. Start camera preview Activity. Snackbar.make(mLayout, "Camera permission was granted. Starting preview.", Snackbar.LENGTH_SHORT) .show(); startCamera(); } else { // Permission request was denied. Snackbar.make(mLayout, "Camera permission request was denied.", Snackbar.LENGTH_SHORT) .show(); } } }
测试代码:
package com.example.joy.granttest; import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.EditText; import android.widget.Toast; public class MainActivity extends AppCompatActivity { private EditText mEtNumber; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mEtNumber = (EditText) findViewById(R.id.et_number); } public void onDialClick(View v) { switch (v.getId()) { case R.id.btn_dial: if (mEtNumber.getText() == null) { Toast.makeText(MainActivity.this, "number is null !", Toast.LENGTH_SHORT).show(); return; } if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { if (ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this, Manifest.permission.CALL_PHONE)) { } ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE}, 200); } else { callPhone(); } break; default: break; } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == 200) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { callPhone(); } else { Toast.makeText(MainActivity.this, "Pemission Denied!", Toast.LENGTH_SHORT).show(); } } super.onRequestPermissionsResult(requestCode, permissions, grantResults); } private void callPhone() { String phoneNumber = mEtNumber.getText().toString(); Intent intent = new Intent(Intent.ACTION_CALL); Uri uri = Uri.parse("tel:" + phoneNumber); intent.setData(uri); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) { // TODO: Consider calling // ActivityCompat#requestPermissions // here to request the missing permissions, and then overriding // public void onRequestPermissionsResult(int requestCode, String[] permissions, // int[] grantResults) // to handle the case where the user grants the permission. See the documentation // for ActivityCompat#requestPermissions for more details. return; } startActivity(intent); } }