Android安全架构规定:默认情况下,任何应用都没有权限执行对其他应用、操作系统或用户有不利影响的任何操作。这包括读写用户的私有数据(如联系人或电子邮件等)、读写其他应用的文件、执行网络访问、使设备保持唤醒状态等等。
如果要使用这些受保护的设备功能,首先要在应用的清单文件(AndroidManifest.xml)中添加一个或多个
标记:
"http://schemas.android.com/apk/res/android"
package="com.android.app.myapp" >
"android.permission.RECEIVE_SMS" />
...
如果您的应用在其清单中列出正常权限(即不会对用户隐私或设备操作造成很大风险的权限),系统会自动授予这些权限。如果您的应用在其清单中列出危险权限(即可能影响用户隐私或设备正常操作的权限),系统会要求用户明确授予这些权限。Android 发出权限请求的方式取决于系统版本:
通常,权限失效会导致SecurityException
被扔回应用。但不能保证每个地方都是这样。例如,sendBroadcast(Intent)
方法在数据传递到每个接收者时会检查权限,在方法调用返回后,即使权限失效,您也不会收到异常。但在几乎所有情况下,权限失效都会打印到系统日志。
注意:从Android 6.0(Marshmallow,API 23)开始,用户可以在任何时候撤销应用的某个权限,即使应用的targetSdkVersion小于23。因此你一定要好好地测试一下你的应用以保证在请求授权失败时应用表现良好,无论你的应用的API等级是多少。
系统权限分为几个保护级别。需要了解的两个最重要保护级别是正常权限和危险权限:
正常权限(Normal 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
危险权限(Dangerous permissions):危险权限涵盖了应用需要那些涉及用户隐私信息的数据或资源或者可能对用户存储的数据或其他应用的操作产生影响的区域。例如,读取用户的联系人的权限就属于危险权限。如果应用声明其需要危险权限,则用户必须明确向应用授予该权限。
为了更好地对权限进行管理和描述,将权限进行分组,所有危险的Android系统权限都属于权限组。
如果设备运行的是Android 6.0(Marshmallow,API 23)或更高版本,并且应用的targetSdkVersion是23或更高版本,则当用户请求危险权限时系统会发生以下行为:
READ_CONTACTS
权限,系统会弹出对话框告知用户应用需要访问设备的联系人,如果用户允许授权,那么系统将授予应用所需的权限。READ_CONTACTS
权限,那么之后它请求WRITE_CONTACTS
时系统将立即授予该权限。 如果设备运行的是Android 5.1(LOLLIPOP_MR1,API 22)或更低版本,并且应用的targetSdkVersion是22或更低版本,则系统会在安装时要求用户授予权限。再次强调,系统只告诉用户应用需要的权限组,而不告知具体权限。
任何权限都可以属于一个权限组,包括正常权限和应用自定义的权限。但权限组仅当权限危险时才影响用户体验。可以忽略正常权限的权限组。
Permission Group | Permissions |
---|---|
CALENDAR |
|
CAMERA |
|
CONTACTS |
|
LOCATION |
|
MICROPHONE |
|
PHONE |
|
SENSORS |
|
SMS |
|
STORAGE |
|
从Android 6.0(Marshmallow,API 23)开始,用户就可以在应用运行时而不是安装时授予应用权限了。由于用户不需要在安装和升级应用时给应用授权,应用的安装升级也更加流畅了。
也就是说,第一次请求权限时,系统会向用户弹出一个权限请求对话框,如:
如果用户点击了拒绝,那么之后shouldShowRequestPermissionRationale()
方法将返回true
,并且再次请求该权限时会出现"不再询问"复选框(Don't ask again):
如果没有勾选"不再询问"并点击了拒绝,那么再次请求该权限时系统依然会向用户显示权限请求对话框。如果此时勾选了"不再询问"并点击了拒绝,那么之后shouldShowRequestPermissionRationale()
方法将返回false
并且请求该权限时系统将不会向用户显示权限请求对话框(系统会立即拒绝该权限请求并调用你的onRequestPermissionsResult()
回调方法并传递PERMISSION_DENIED
):
为了简化版本判断等代码逻辑,我们可以直接使用Android支持库中的API来检查和请求权限。
如果你的应用需要一个危险权限,你每次执行该权限下的操作时都必须检查你是否被授予了该权限。用户可以随便撤销某个权限,所以即使应用昨天还能使用相机,那也没法保证今天也有权使用。
为了检查当前是否拥有该权限,可以调用ContextCompat.checkSelfPermission()
方法,例如,下面代码就是检查该Activity是否有权写calendar日程:
// 假设thisActivity就是当前activity
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.WRITE_CALENDAR);
如果应用当前拥有该权限,该方法返回PackageManager.PERMISSION_GRANTED
,应用可以继续接下来的操作。如果应用当前没有该权限,该方法返回PackageManager.PERMISSION_DENIED
,应用就必须明确地向用户请求授权。
一些情况下,你可能想要帮助用户理解为什么你的应用需要某个权限。例如,如果一个用户安装了一个拍照App,那么用户不会因App请求相机权限而惊讶,但用户可能不会理解为什么App还要访问他的位置或联系人。因此,在请求权限之前,你应该考虑给用户一个解释。但要注意,如果你解释的太多用户可能会很厌烦并卸载应用。
一种情景,就是用户已经关闭了权限的请求。如果用户始终尝试使用该权限下的功能,但是每次都关闭该权限请求,这就表明用户不理解为什么app需要这个权限才能工作,在这种情况下,向用户弹窗解释一下可能是个很好的主意:
为了有助于在合适场景给用户一个解释,Android提供了一个工具方法shouldShowRequestPermissionRationale()
,如果应用之前请求过该权限但用户拒绝了该方法就会返回true
。
注意:如果用户之前拒绝了权限请求并且勾选了权限请求对话框的”不再询问”,该方法会返回
false
,如果设备策略禁止该应用获得该权限也会返回false
。
如果你的应用没有获得所需的权限,应用就必须调用requestPermissions()
方法请求相应的权限。传过去权限列表和一个整型权限请求码,该方法是异步的:在用户操作完请求授权对话框后系统会调用应用的回调方法传给结果和之前requestPermissions()
中的权限请求码:
// 这里的thisActivity就是当前activity
if (ContextCompat.checkSelfPermission(thisActivity,
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常量,用户唯一标识一个权限请求以便回调时进行判断
}
}
注意:当你的应用调用
requestPermissions()
方法时,系统会向用户展示一个标准对话框,你的应用不能修改也不能自定义这个对话框,如果你需要给用户一些额外的信息和解释你就需要在调用requestPermissions()
之前像上面一样" 解释为什么应用需要这些权限"。
当你的应用请求权限时,系统会向用户显示一个权限请求对话框。当用户响应时,系统会调用应用的onRequestPermissionsResult()
方法并把用户响应传给它。这就要求你的应用重写该方法以判断权限是否被授予了。这个回调也会接受之前调用requestPermissions()
方法时的请求码。例如如果你的应用请求READ_CONTACTS
了权限那么回调方法可能会是这样:
@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;
}
// case其它权限请求...
}
}
当系统请求用户授予权限时,用户可以选择告诉系统不要再请求该权限了。这种情况下,无论应用在什么时候使用requestPermissions()
再次请求该权限,系统都会立即拒绝该请求。系统会调用你的onRequestPermissionsResult()
回调方法并传递PERMISSION_DENIED
,如果用户再次明确拒绝了你的请求,系统将采用相同方式操作。这意味着当你调用requestPermissions()
时,你不能假设已经发生了与用户的任何直接交互。
为了让应用有更好的用户体验,动态请求权限有两个场景要特别注意,一个就是用户多次拒绝权限申请,一个就是用户点击了拒绝并勾选了不再询问或者设备策略禁止该应用获得该权限。这两种情况下,如果用户还想使用该权限下的功能就需要你的应用自己定制用户引导了。
第一种情况,是因为用户不理解你为什么要申请这个权限才拒绝你的权限申请,所以这种情况下,你可以在请求权限(弹出权限请求对话框)之前向用户显示一个对话框向用户解释为什么你的应用需要这个权限:
当用户点击确定后再请求权限(弹出权限请求对话框)用户就很容易授予该权限了。至于什么时候显示该对话框上面的"解释为什么应用需要这些权限"已经说得很清楚的,即当shouldShowRequestPermissionRationale()
返回true
时显示这个对话框:
public class RationaleDialogFragment extends DialogFragment {
public static final String TAG = "RationaleDialogFragment";
private static final String ARG_POSITIVE_BUTTON = "positiveButton";
private static final String ARG_NEGATIVE_BUTTON = "negativeButton";
private static final String ARG_RATIONALE_MESSAGE = "rationaleMsg";
private static final String ARG_REQUEST_CODE = "requestCode";
private static final String ARG_PERMISSIONS = "permissions";
private int positiveButton;
private int negativeButton;
private String rationaleMsg;
private int requestCode;
private String[] permissions;
private PermissionCallbacks mPermissionCallbacks;
public RationaleDialogFragment() {
}
public static RationaleDialogFragment newInstance(
@StringRes int positiveButton, @StringRes int negativeButton,
@NonNull String rationaleMsg, int requestCode, @NonNull String[] permissions) {
RationaleDialogFragment fragment = new RationaleDialogFragment();
Bundle args = new Bundle();
args.putInt(ARG_POSITIVE_BUTTON, positiveButton);
args.putInt(ARG_NEGATIVE_BUTTON, negativeButton);
args.putString(ARG_RATIONALE_MESSAGE, rationaleMsg);
args.putInt(ARG_REQUEST_CODE, requestCode);
args.putStringArray(ARG_PERMISSIONS, permissions);
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
boolean isAtLeastJellyBeanMR1 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1;
if (isAtLeastJellyBeanMR1
&& getParentFragment() != null
&& getParentFragment() instanceof PermissionCallbacks) {
mPermissionCallbacks = (PermissionCallbacks) getParentFragment();
} else if (context instanceof PermissionCallbacks) {
mPermissionCallbacks = (PermissionCallbacks) context;
} else {
throw new RuntimeException(context.toString()
+ " must implement PermissionCallbacks");
}
}
@Override
public void onDetach() {
super.onDetach();
mPermissionCallbacks = null;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
if (getArguments() != null) {
positiveButton = getArguments().getInt(ARG_POSITIVE_BUTTON);
negativeButton = getArguments().getInt(ARG_NEGATIVE_BUTTON);
rationaleMsg = getArguments().getString(ARG_RATIONALE_MESSAGE);
requestCode = getArguments().getInt(ARG_REQUEST_CODE);
permissions = getArguments().getStringArray(ARG_PERMISSIONS);
}
setCancelable(false);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setCancelable(false)
.setPositiveButton(positiveButton, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Object host;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
host = getParentFragment() != null ?
getParentFragment() :
getActivity();
} else {
host = getActivity();
}
if (host instanceof Fragment) {
((Fragment) host).requestPermissions(permissions, requestCode);
} else if (host instanceof FragmentActivity) {
ActivityCompat.requestPermissions(
(FragmentActivity) host, permissions, requestCode);
}
}
})
.setNegativeButton(negativeButton, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (mPermissionCallbacks != null) {
mPermissionCallbacks.onPermissionsDenied(requestCode,
Arrays.asList(permissions));
}
}
})
.setMessage(rationaleMsg);
return builder.create();
}
public interface PermissionCallbacks extends ActivityCompat.OnRequestPermissionsResultCallback {
void onPermissionsGranted(int requestCode, List perms);
void onPermissionsDenied(int requestCode, List perms);
}
}
public class MainActivity extends AppCompatActivity implements RationaleDialogFragment.PermissionCallbacks {
public static final String TAG = "MainActivity";
private static final int RC_CAMERA = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Log.i(TAG, "onRequestPermissionsResult:" + requestCode + "," + Arrays.toString(permissions) + "," + Arrays.toString(grantResults));
switch (requestCode) {
case RC_CAMERA: {
// 如果请求被取消了,那么结果数组就是空的
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限被授予了
showCameraPreview();
}
return;
}
}
}
public void showCamera(View view) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 我们应该给用户个解释?
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
// 向用户显示一个解释,要以异步非阻塞的方式
// 该线程将等待用户响应!等用户看完解释后再继续尝试请求权限
RationaleDialogFragment
.newInstance(android.R.string.ok, android.R.string.cancel,
getString(R.string.rationale_camera), RC_CAMERA,
new String[]{Manifest.permission.CAMERA})
.show(getSupportFragmentManager(), RationaleDialogFragment.TAG);
} else {
// 不需要向用户解释了,我们可以直接请求该权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
RC_CAMERA);
}
} else {
showCameraPreview();
}
}
private void showCameraPreview() {
getSupportFragmentManager().beginTransaction()
.replace(R.id.content_fragment, CameraPreviewFragment.newInstance())
.addToBackStack("camera")
.commit();
}
@Override
public void onPermissionsGranted(int requestCode, List perms) {
Log.i(TAG, "onPermissionsGranted:" + requestCode + ":" + perms.size());
}
@Override
public void onPermissionsDenied(int requestCode, List perms) {
Log.i(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());
}
}
第二种情况,系统已经无法在申请权限时向用户弹出权限申请框,你可以显示一个对话框引导用户跳转到"设置>应用>[你的应用]"页面让用户手动授予该权限:
当用户点击确定后跳转到应用详情页让用户手动授予相应权限:
至于什么时候显示该对话框也很明显,即该权限申请被永久拒绝(可能是因为用户之前拒绝了该权限申请并勾选了不再询问,也可能是设备策略禁止了该应用的授权请求)时显示该对话框,也就是当系统调用你的onRequestPermissionsResult()
回调方法并传递PERMISSION_DENIED
时shouldShowRequestPermissionRationale
返回false
时显示该对话框:
public class AppSettingsDialogFragment extends DialogFragment {
public static final String TAG = "AppSettingsDialogFragment";
public static final int DEFAULT_SETTINGS_REQ_CODE = 16061;
private static final String ARG_POSITIVE_BUTTON = "positiveButton";
private static final String ARG_NEGATIVE_BUTTON = "negativeButton";
private static final String ARG_RATIONALE_TITLE = "rationaleTitle";
private static final String ARG_RATIONALE_MESSAGE = "rationaleMsg";
private static final String ARG_REQUEST_CODE = "requestCode";
private int positiveButton;
private int negativeButton;
private String rationaleTitle;
private String rationaleMsg;
private int requestCode;
public AppSettingsDialogFragment() {
}
public static AppSettingsDialogFragment newInstance(
@StringRes int positiveButton, @StringRes int negativeButton,
@NonNull String rationaleTitle, @NonNull String rationaleMsg,
int requestCode) {
AppSettingsDialogFragment fragment = new AppSettingsDialogFragment();
Bundle args = new Bundle();
args.putInt(ARG_POSITIVE_BUTTON, positiveButton);
args.putInt(ARG_NEGATIVE_BUTTON, negativeButton);
args.putString(ARG_RATIONALE_TITLE, rationaleTitle);
args.putString(ARG_RATIONALE_MESSAGE, rationaleMsg);
args.putInt(ARG_REQUEST_CODE, requestCode);
fragment.setArguments(args);
return fragment;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
if (getArguments() != null) {
positiveButton = getArguments().getInt(ARG_POSITIVE_BUTTON);
negativeButton = getArguments().getInt(ARG_NEGATIVE_BUTTON);
rationaleTitle = getArguments().getString(ARG_RATIONALE_TITLE);
rationaleMsg = getArguments().getString(ARG_RATIONALE_MESSAGE);
requestCode = getArguments().getInt(ARG_REQUEST_CODE);
}
setCancelable(false);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setCancelable(false)
.setTitle(rationaleTitle)
.setMessage(rationaleMsg)
.setPositiveButton(positiveButton, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (getContext() instanceof Activity) {
Activity activity = (Activity) getContext();
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", activity.getPackageName(), null);
intent.setData(uri);
activity.startActivityForResult(intent, requestCode);
}
}
})
.setNegativeButton(negativeButton, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
});
return builder.create();
}
}
public class MainActivity extends AppCompatActivity implements RationaleDialogFragment.PermissionCallbacks {
public static final String TAG = "MainActivity";
private static final int RC_CAMERA = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
Log.i(TAG, "onRequestPermissionsResult:" + requestCode + "," + Arrays.toString(permissions) + "," + Arrays.toString(grantResults));
switch (requestCode) {
case RC_CAMERA: {
// 如果请求被取消了,那么结果数组就是空的
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限被授予了
showCameraPreview();
} else {
// 权限被拒绝了
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
// 向用户显示一个对话框引导用户跳转到"设置>应用>[你的应用]"页面
// 让用户手动授予该权限
AppSettingsDialogFragment
.newInstance(android.R.string.ok, android.R.string.cancel,
getString(R.string.permissions_required),
getString(R.string.rationale_ask_again),
AppSettingsDialogFragment.DEFAULT_SETTINGS_REQ_CODE)
.show(getSupportFragmentManager(), AppSettingsDialogFragment.TAG);
}
}
}
return;
}
}
}
public void showCamera(View view) {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 我们应该给用户个解释?
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
// 向用户显示一个解释,要以异步非阻塞的方式
// 该线程将等待用户响应!等用户看完解释后再继续尝试请求权限
RationaleDialogFragment
.newInstance(android.R.string.ok, android.R.string.cancel,
getString(R.string.rationale_camera), RC_CAMERA,
new String[]{Manifest.permission.CAMERA})
.show(getSupportFragmentManager(), RationaleDialogFragment.TAG);
} else {
// 不需要向用户解释了,我们可以直接请求该权限
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
RC_CAMERA);
}
} else {
showCameraPreview();
}
}
private void showCameraPreview() {
getSupportFragmentManager().beginTransaction()
.replace(R.id.content_fragment, CameraPreviewFragment.newInstance())
.addToBackStack("camera")
.commit();
}
@Override
public void onPermissionsGranted(int requestCode, List perms) {
Log.i(TAG, "onPermissionsGranted:" + requestCode + ":" + perms.size());
}
@Override
public void onPermissionsDenied(int requestCode, List perms) {
Log.i(TAG, "onPermissionsDenied:" + requestCode + ":" + perms.size());
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == AppSettingsDialogFragment.DEFAULT_SETTINGS_REQ_CODE) {
Log.i(TAG, "onActivityResult:" + requestCode + ":" + data);
}
}
}
当然,Framework和Support Library中的Activity/Fragment
的代码实现细节会有些不一样,针对多个权限的同时请求也需要额外的判断和处理,可以写一些Helper类封装一下核心逻辑,这一点要特别推荐EasyPermissions这个Lib,封装的也比较优雅。
最后,是一些建议:
onActivityResult()
方法中接收结果就可以了。References