Android权限机制简述及动态权限管理的一种解决方案
简述
Android是一个权限分离的操作系统,每一个应用程序运行时都会有一个明确地系统身份标识(Linux的user ID和group ID)。部分系统也同样被特定身份标识而隔开。因此,Linux才能将应用程序与其他程序和系统隔离开来。
这样的机制可以说是相当安全,但是也阻断了各个应用程序之间或者和系统之间的“交流”。因此,Android通过一种“permission”机制强力限制某些特定地操作来达到细粒度的安全能力。
进程沙箱
Android进程沙箱机制是借鉴Linux中用户组的原理,其限制了不同应用程序之间的资源和数据的互访。当应用首次安装的时,系统会向其分配一个UID。如果该应用程序是第三方的,那么其UID值大于10000,如果是系统应用程序则小于10000。如果应用程序卸载后又重新安装,那么其UID值是会改变的。
//获取应用程序UID方法
public void getApplicationUid() {
PackageManager pm = getPackageManager();
try {
ApplicationInfo ai = pm.getApplicationInfo(getPackageName(), PackageManager.GET_ACTIVITIES);
Log.d(getClass().getSimpleName(), "uid = " + ai.uid);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
不同UID的应用程序是不能进行资源互访,从而有效达到进程隔离目的。
Android应用程序的沙箱机制
此外,你可以每个应用程序的AndroidManifest.xml文件中使用ShareUserID属性来使他们拥有同一UserID。UserID相同的应用程序将会被系统当做同一应用程序,拥有相同的UserID和文件权限。
注意:为了保留系统安全性,只有签名相同(并且需要相同的shareUserId)的应用程序才会被分配相同的UserID。
一个应用程序存储的任何数据都会被分配应用程序的UserID,通常是不能被其他应用程序所访问。当使用getSharedPreferences(String, int),openFileOutput(String, int),或者openOrCreateDatabase(String, int, SQLiteDatabase.CursorFactory),你可以使用MODE_WORLD_READABLE或者MODE_WORLD_WRITEABLE标记来允许其他应用程序读或写文件。
权限使用
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.app.myapp" >
<uses-permission android:name="android.permission.RECEIVE_SMS" />
...
</manifest>
权限的使用是相当简单的,某功能需要申请权限时,只需在AndroidManifest.xml文件中申明对应权限就行。如上述代码。
如果你的App在其manifest文件中声明一系列normal permissions(不会对用户隐私或者设备运行构成威胁的权限),系统会自动准许这些权限申请。如果你的App在其manifest文件中声明一系列dangerous permissions(对用户隐私或者设备运行构成潜在威胁的权限),系统将会询问用户是否同意这些权限申请。
询问的方式根据系统的版本而有所不同。
1.静态权限申请询问界面
若设备运行的系统版本为Android5.1(API版本22)或更低,或者App的targetSdkVersion是22或更低,Android提供的是静态权限申请询问界面。
这种询问方式只要玩过Android手机的应该都见过,当应用程序首次安装时,会弹出以下类似界面,出现在图标列表中的权限都是dangerous permissions。
应用程序首次安装权限询问界面
这种询问方式相当霸道,如果想要安装该应用,我们只有同意其申请的所有权限。当应用程序安装更新时,如果该应用程序有新申请的权限,那么该权限询问界面会将新申请的权限列出。你废除这些权限申请的唯一方式就是卸载它们!
-
动态权限申请询问界面
如果设备运行的系统版本为Android6.0(API版本23)或更高,或者App的targetSdkVersion是23或更高,Android提供了动态权限申请询问界面。
其实这种方式,早在Android6.0之前就有大批国产ROM提供动态权限管理方式,市面上主流的安全软件也提供这种功能。Google终于在Android6.0提供了动态权限管理功能(不过对我大天朝来说然并卵)。
动态权限申请询问界面
这种交互方式更加的人性化,也更加安全。在应用程序运行的过程中,如果需要申请网络连接权限,那么系统会弹出权限询问对话框供用户选择。
当然,权限并不仅仅局限于此。我们也可以自定义某些权限来保证安全性。比如,启动Activity或者Service时,增加权限控制,防止被外部应用程序胡乱启动。
权限组
对普通第三方应用程序来说,权限一般分为normal permission和dangerous permission。Android系统所有的dangerous permissions都属于某一权限组。如果设备运行的系统版本为Android6.0(API版本23)或更高,或者App的targetSdkVersion是23或更高,当你的应用程序需要一个dangerous permission时,那么:
- 如果应用程序在manifest中声明了一个dangerous permission,并且它目前没有该权限组中的任一权限,那么系统会弹出一个将要申请权限组的对话框。但是该对话框不会具体描述是该权限组中的哪一个权限。比如应用程序需要READ_CONTACTS权限,那么该对话框仅仅只描述为该应用程序需要访问联系人。
2.如果应用程序在manifest中声明了一个dangerous permission,并且它已经拥有该权限组的其他权限,那么系统将直接允许其访问该权限,不与用户产生交互。
若设备运行的系统版本为Android5.1(API版本22)或更低,或者App的targetSdkVersion是22或更低,系统将会在应用程序安装的时候让用户同意权限申请。系统仅仅只告诉用户哪些权限组被申请,而不是单独某一个权限。
Android6.0动态权限管理
国产ROM和各类安全软件早已提供了动态权限管理功能,实现方式上大同小异,虽然对用户来说这是相当利好的消息,但是对我们开发者来说,还是很麻烦的,各种ROM的兼容性让我们很头疼。终于在棉花糖上,Android提供了动态权限管理的相关API,我们在处理权限问题上方便了很多。
当你的需要申请一个dangerous permission时候,你必须在每次申请之前进行权限检查。权限检查的方法如下。
int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.CALL_PHONE);
如果该方法的返回值为PackageManager.PERMISSION_GRANTED,应用程序就可以继续后续操作。如果应用程序没有该权限,那么方法的返回值为PERMISSION_DENIED,并且将会询问用户是否允许该权限。
我们在Manifest中申请的任何dangerous permission,都会询问用户是否允许该权限,Android提供了几个申请权限的方法,调用之后,会弹出一个标准的系统对话框供用户选择,该对话框是不能自定义的。
如果一个图像类软件申请发短信权限,用户可能会产生怀疑,是不是扣费短信。那么我们如何降低用户的猜疑呢?Android提供了一个比较实用的方法shouldShowRequestPermissionRationale(),该方法给了我们一个解释的机会来增加权限申请通过的概率。如果该权限之前已被申请过但是被用户拒绝,那么shouldShowRequestPermissionRationale()方法返回true。
如果你的应用程序没有所需要的权限,那么你必须要通过调用requestPermissions()方法来申请权限,该方法调用后,系统会立刻弹出权限申请询问对话框供供用户选择,在用户交互后,系统会立刻通过onRequestPermissionsResult()将结果返回给应用程序。这里直接将官方文档中相关演示代码贴出来供参考。
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS)) {
// Show an expanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
} else {
// No explanation needed, we can request the permission.
ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);
// MY_PERMISSIONS_REQUEST_READ_CONTACTS is an
// app-defined int constant. The callback method gets the
// result of the request.
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay! Do the
// contacts-related task you need to do.
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
}
return;
}
// other 'case' lines to check for other
// permissions this app might request
}
}
再一次,系统权限对话框仅仅只描述你所申请权限所在权限组的描述,而不是针对某一特定权限。对于同一权限组的权限,用户只需同意一次即可。这种方案的好坏见仁见智,但是有时候感觉会把一个很小的问题给扩大了,比如我们只是需要简单的获取设备的IMEI码,那么这时候系统对话框的描述为应用程序将访问设备信息。这时候用户肯定会想,你访问我设备信息作甚!然后你的申请被无情拒绝了!
注意:你的应用程序需要明确地申请每一个你需要的权限,即使用户已经同意了该权限所在权限组的另外一个权限。此外,随着Android版本的更新,权限组中所含的权限可能会改变。因此,不要偷懒,该显示申请权限的地方还是要乖乖申请吧。
合理申请权限
曾几何时,权限的滥用导致用户隐私泄露频发,而今,用户对隐私也愈发敏感,过渡的权限申请会给用户造成不良的印象。因此,作为有节操的程序员,我们在权限申请上应该慎重,而不是一股脑把所有权限都给申请。
随着Android版本的更新,相应的权限也会更新,因此我们一定要注意不同targetSdkVersion属性所带来的权限变化,并尽可能的提高targetSdkVersion。在权限使用上Google也给了我们一些建议。
- 考虑使用Intent来完成权限相关的操作
这点建议,我觉得可以作为一个比较好的参考。在Manifest中,我们申请了SEND_SMS权限,那么可以通过下面代码完成发送短信功能。
SmsManager sm = SmsManager.getDefault();
sm.sendTextMessage(address, null, message, null, null);
如果发送短信时候,用户选择拒绝该权限申请,那么你的功能也就Over了。
如果我们换intent方式进行发送短信,则不会出现权限被拒绝的情况,代码如下。
Intent intent = new Intent(Intent.ACTION_SENDTO);
intent.setData(Uri.parse("smsto:" + number));
intent.putExtra("sms_body", body);
context.startActivity(sendIntent);
该方法会跳转到发送短信界面(如果系统装有多个短信类应用,那么系统会弹出一个选择应用对话框,让用户选择使用何种应用来完成发送短信功能),并填充好相应的内容。类似的,拨打电话和使用照相机等都可以使用intent来完成相应的功能,降低了用户拒绝权限的风险。
最后,权限方式和intent方式各有千秋,根据不同的业务情景,我们可以选择不同的方式。
- 只申请你所需要的权限
不想让用户觉得你的应用程序是一个“流氓应用”,最好不要过度申请权限。
3.不要“吞噬”用户
在Android6.0中,不要在同一时刻申请多种权限。因为系统可能会弹出多个系统权限询问对话框,这种情况:
第一,用户可能觉得很烦锁,并退出你的应用程序。
第二,用户可能由于误操作,拒绝了你的某些权限申请。
因此,最好的方式还是在你需要的时候进行申请吧。
4.给出你为什么使用权限的原因
为了降低权限申请被拒绝的风险,最好在调用requestPermissions()之前,进行权限申请的说明,使用户觉得你不是在做“坏事”。
动态权限申请的一种解决方案
虽然目前Android6.0市场占有率相当低,但是随着时间的推移,关于动态权限管理这一块,我们迟早要接触的。这里我参考Android官方开发文档,封装了动态权限管理所需的方法。虽然自己的项目中还未用到动态权限管理,但作为工作之余的学习还是大有裨益!
权限申请流程
权限申请流程图BaseActivity中完成权限申请
这里我没有将权限申请相关方法封装成一个类,而是在BaseActivity中添加相关方法。
public class BaseActivity extends AppCompatActivity {
//申请请求的request code
private final static int YZT_PERMISSION_REQUEST = 12;
public final String TAG = getClass().getSimpleName();
//是否跳转过应用程序信息详情页
private boolean mIsJump2Settings = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
if (mIsJump2Settings) {
onRecheckPermission();
mIsJump2Settings = false;
}
}
//单个权限的检查
public void checkPermission(@NonNull final String permission, @Nullable String reason) {
if (Build.VERSION.SDK_INT < 23) return;
int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
//权限已经申请
onPermissionGranted(permission);
} else {
if (!TextUtils.isEmpty(reason)) {
//判断用户先前是否拒绝过该权限申请,如果为true,我们可以向用户解释为什么使用该权限
if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
//这里的dialog可以自定义
new AlertDialog.Builder(this).setCancelable(false).setTitle("温馨提示").setMessage(reason).
setNegativeButton("我知道了", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
requestPermission(new String[]{permission});
dialog.dismiss();
}
}).show();
} else {
requestPermission(new String[]{permission});
}
} else {
requestPermission(new String[]{permission});
}
}
}
//多个权限的检查
public void checkPermissions(@NonNull String... permissions) {
if (Build.VERSION.SDK_INT < 23) return;
//用于记录权限申请被拒绝的权限集合
List<String> permissionDeniedList = new ArrayList<>();
for (String permission : permissions) {
int permissionCheck = ContextCompat.checkSelfPermission(this, permission);
if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
onPermissionGranted(permission);
} else {
permissionDeniedList.add(permission);
}
}
if (!permissionDeniedList.isEmpty()) {
String[] deniedPermissions = permissionDeniedList.toArray(new String[permissionDeniedList.size()]);
requestPermission(deniedPermissions);
}
}
//调用系统API完成权限申请
private void requestPermission(String[] permissions) {
ActivityCompat.requestPermissions(this, permissions, YZT_PERMISSION_REQUEST);
}
//申请权限被允许的回调
public void onPermissionGranted(String permission) {
}
//申请权限被拒绝的回调
public void onPermissionDenied(String permission) {
}
//申请权限的失败的回调
public void onPermissionFailure() {
}
//如果从设置界面返回,则重新申请权限
public void onRecheckPermission() {
}
//弹出系统权限询问对话框,用户交互后的结果回调
@Override
public final void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case YZT_PERMISSION_REQUEST:
if (grantResults.length > 0) {
//用于记录是否有权限申请被拒绝的标记
boolean isDenied = false;
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
onPermissionGranted(permissions[i]);
} else {
isDenied = true;
onPermissionDenied(permissions[i]);
}
}
if (isDenied) {
isDenied = false;
//如果有权限申请被拒绝,则弹出对话框提示用户去修改权限设置。
showPermissionSettingsDialog();
}
} else {
onPermissionFailure();
}
break;
}
}
private void showPermissionSettingsDialog() {
new AlertDialog.Builder(this).setCancelable(false).setTitle("温馨提示").
setMessage("缺少必要权限\n不然将导致部分功能无法正常使用").setNegativeButton("下次吧", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
}).setPositiveButton("去设置", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
jump2PermissionSettings();
}
}).show();
}
/**
* 跳转到应用程序信息详情页面
*/
private void jump2PermissionSettings() {
mIsJump2Settings = true;
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
}
使用方法
public class MainActivity extends BaseActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//权限检查
String[] permissionArray = {Manifest.permission.SEND_SMS, Manifest.permission.CALL_PHONE};
checkPermissions(permissionArray);
// checkPermission(Manifest.permission.SEND_SMS, "YZT将要发生短信进行身份验证");
}
@Override
public void onRecheckPermission() {
super.onRecheckPermission();
String[] permissionArray = {Manifest.permission.SEND_SMS, Manifest.permission.CALL_PHONE};
checkPermissions(permissionArray);
}
@Override
public void onPermissionGranted(String permission) {
super.onPermissionGranted(permission);
switch (permission) {
case Manifest.permission.SEND_SMS:
//TODO:发送短信
Toast.makeText(this, "发短信咯", Toast.LENGTH_LONG).show();
break;
case Manifest.permission.CALL_PHONE:
//TODO:打电话
Toast.makeText(this, "电话咯", Toast.LENGTH_LONG).show();
break;
}
}
@Override
public void onPermissionDenied(String permission) {
super.onPermissionDenied(permission);
switch (permission) {
case Manifest.permission.SEND_SMS:
//TODO:
break;
case Manifest.permission.CALL_PHONE:
//TODO:
break;
}
}
@Override
public void onPermissionFailure() {
super.onPermissionFailure();
Toast.makeText(this, "权限获取失败", Toast.LENGTH_LONG).show();
}
随着用户安全意识的提升,我们在权限的使用上也应该更加趋于合理和谨慎。虽然目前Android6.0的占有率很低,但是我们也应该未雨绸缪,尽快引入动态权限管理机制。