好久之前在网上看到国外一篇关于M的权限机制详解,由于是英文的,刚好最近闲的没事做,顺手翻译一下。
前几天官方公布了Android M的名字,最终版本也将在不久后发布。
虽然Android还在不断发生着变化,最新M版本的更新和以往是完全不同的,因为有一些重要的更新,例如运行时权限。令人惊奇的是,在Android开发社区这并没有过多相关讨论,即使这是相当重要而且可能会引起一些大麻烦。
这也正是我决定写一篇博客来讨论这个主题的原因。趁现在还不算太晚,你将从这里全面了解运行时权限包括如何在代码中如何实现 。
新的运行时权限
Andorid的权限管理系统一直都是被关注最多的安全问题之一,因为一旦软件被安装就能在用户没有任何感知的情况下获得所有申明的权限 。
正因如此,一些意图不轨的人能利用系统的这块软肋搜集用户隐私信息,并用在不正当的途径里。
Android团队当然知道这个令人紧急的事态,自Android问世7年后的今天,在Android6.0棉花糖版本里,当应用程序在安装期间将不再被授予任何权限(应该是指危险权限),取而代之的是,应用程序必须在使用的时候向用户申请所需权限。
需要注意的是,申请权限的Dialog并不会自动弹出,开发者必须手动申明调用,如果开发者申请的权限没有获得用户允许,则当使用该权限相关功能的时候应用程序会突然抛出异常导致崩溃
此外,用户还能在任何时候通过手机内的设置模块来撤消应用程序已被授权的权限。
估计你现在已经蛋碎了...如果你是个Android开发者,你应该会意识到程序逻辑已经发生了巨大的变化,你不能再像以前那样进行开发,因为你必须去检查每个权限否则分分钟给你崩溃!
正因如此,我需要提醒你这不是一件简单的工作。虽然这对用户来说是一件好事,但对我们开发者来说简直就是噩梦,我们必须在下个阶段采取行动,否则无论从短期还是长期角度来说这都是个巨大的问题。
尽管如此,新的运行时权限机制只在应用程序的targetSdkVersion>=23时生效,并且只在6.0系统之上有这种机制,在低于6.0的系统上应用程序和以前一样不受影响。
之前发布/安装的应用程序会怎样?
新的运行时权限可能已经让你感到恐慌,你可能会问:“嘿~如果我的应用程序是在3年前就已经发布/安装了,在6.0系统的设备上是否也会崩溃?”
小伙子别着急,Android团队早就考虑过这种情况。如果应用程序的targetSdkVersion小于23,系统会默认其尚未适配新的运行时权限机制,安装后将和以前一样不受影响:即用户在安装应用程序的时候默认允许所有被申明的权限 !
喏,应用程序和以前一样没受影响。但是!你需要注意的是!用户在安装后仍然可以撤消任何权限 !即使6.0系统会在用户撤消权限时给予警告,但是用户仍然能执行撤消的操作!
那么接下来你的脑子中就会提出一个问题:那这样我的应用程序会崩溃么?
Android团队还是像上帝般仁慈滴~当应用程序targetSdkVersion小于23时,其某个功能在调用已被用户撤消的权限时是不会抛出任何异常的,只是该功能不会有任何反应,对于功能的返回值来说,在这种情况下它只会return0或者空。
但是不要高兴的太早,虽然应用程序在调用功能的时候不会崩溃,但是有可能因为该功能不正常的返回值(0或null)引起崩溃!
好消息(到现在为止来看)是因为这种机制是全新的,我相信很少会有用户去这么做,万一他们这么做了,那他们就必须要接受相应的结果。
但是从长远角度来看,我相信会有大量的用户会选择禁止某些权限。我们的应用程序在新的设备上无法完美运行,这是不能忍的!
为了使程序能完美运行,你最好修改程序去适配新的权限机制,并且现在就开干!
对于哪些还没有完美适配运行时权限机制的应用程序来说,千万不要将targetSdkVersion提升到23,否则你会有大麻烦哟,只有当你通过了所有测试才能将其targetSdkVersion设置为23。
警告:现在你新建项目的时候,Studio会默认将targetSdkVersion设置为23,如果你还没完全适配运行时权限机制,我建议你还是将targetSdkVersion设置为22。
自动授权的权限(默认无需申请的权限)
下面列出的权限代表应用程序在安装的时候会自动被允许使用且无法被撤消的权限,我们称之为Normal Permission(PROTECTION_NORMAL):
android.permission.ACCESS_LOCATION_EXTRA_COMMANDS
android.permission.ACCESS_NETWORK_STATE
android.permission.ACCESS_NOTIFICATION_POLICY
android.permission.ACCESS_WIFI_STATE
android.permission.ACCESS_WIMAX_STATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE_NETWORK_STATE
android.permission.CHANGE_WIFI_MULTICAST_STATE
android.permission.CHANGE_WIFI_STATE
android.permission.CHANGE_WIMAX_STATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND_STATUS_BAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET_PACKAGE_SIZE
android.permission.INTERNET
android.permission.KILL_BACKGROUND_PROCESSES
android.permission.MODIFY_AUDIO_SETTINGS
android.permission.NFC
android.permission.READ_SYNC_SETTINGS
android.permission.READ_SYNC_STATS
android.permission.RECEIVE_BOOT_COMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST_INSTALL_PACKAGES
android.permission.SET_TIME_ZONE
android.permission.SET_WALLPAPER
android.permission.SET_WALLPAPER_HINTS
android.permission.SUBSCRIBED_FEEDS_READ
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE_SYNC_SETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT
你只需要在清单文件AndroidManifest.xml
中申明即可,无需再申请且无法被用户撤消。
让你的应用程序适配运行时权限机制
是时候去让我们的程序完美适配运行时权限机制了。首先将compileSdkVersion
和 targetSdkVersion
设置为23.
1
2
3
4
5
6
7
8
9
|
android {
compileSdkVersion
23
...
defaultConfig {
...
targetSdkVersion
23
...
}
|
举个例子,我们尝试实现一个添加联系人的功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
private
static
final
String TAG =
"Contacts"
;
private
void
insertDummyContact() {
// Two operations are needed to insert a new contact.
ArrayList<ContentProviderOperation> operations =
new
ArrayList<ContentProviderOperation>(
2
);
// First, set up a new raw contact.
ContentProviderOperation.Builder op =
ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE,
null
)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME,
null
);
operations.add(op.build());
// Next, set the name for the contact.
op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID,
0
)
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,
"__DUMMY CONTACT from runtime permissions sample"
);
operations.add(op.build());
// Apply the operations.
ContentResolver resolver = getContentResolver();
try
{
resolver.applyBatch(ContactsContract.AUTHORITY, operations);
}
catch
(RemoteException e) {
Log.d(TAG,
"Could not add a new contact: "
+ e.getMessage());
}
catch
(OperationApplicationException e) {
Log.d(TAG,
"Could not add a new contact: "
+ e.getMessage());
}
}
|
上述代码需要 WRITE_CONTACTS
权限。如果没有去申请就调用该功能,程序会突然崩溃。
下一步是按照老办法在 AndroidManifest.xml
中添加对应的权限。
1
|
<
uses-permission
android:name
=
"android.permission.WRITE_CONTACTS"
/>
|
下一步,我们必须实现一个检测权限是否被允许的功能, 如果没有被允许就需要弹出Dialog向用户申请该权限,然后你才能执行下面添加联系人的操作。
下面的列表代表权限组Permission Group(即同类权限被划分为同一组权限,申请权限时可以直接申请权限组)
如果一个权限组中任一权限被允许,同组内其他权限也同时被允许使用。这种情况下,一旦 WRITE_CONTACTS
被授权,应用权限将同时允许READ_CONTACTS
和 GET_ACCOUNTS
用于检测和申请权限的代码在 Activity的 checkSelfPermission
和requestPermissions
方法里, 这些都属于API23新增的方法。
1
2
3
4
5
6
7
8
9
10
11
|
final
private
int
REQUEST_CODE_ASK_PERMISSIONS =
123
;
private
void
insertDummyContactWrapper() {
int
hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
if
(hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
requestPermissions(
new
String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return
;
}
insertDummyContact();
}
|
如果权限已被授权, insertDummyContact()
将会立即执行,否则,requestPermissions
将会被执行,此时会弹出下图Dialog去申请权限:
无论权限申请被允许还是拒绝,Activity的 onRequestPermissionsResult
方法都会被调用,通过第三个参数 grantResults
可以获取申请结果,如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Override
public
void
onRequestPermissionsResult(
int
requestCode, String[] permissions,
int
[] grantResults) {
switch
(requestCode) {
case
REQUEST_CODE_ASK_PERMISSIONS:
if
(grantResults[
0
] == PackageManager.PERMISSION_GRANTED) {
// Permission Granted
insertDummyContact();
}
else
{
// Permission Denied
Toast.makeText(MainActivity.
this
,
"WRITE_CONTACTS Denied"
, Toast.LENGTH_SHORT)
.show();
}
break
;
default
:
super
.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
|
这就是运行时权限所需做的基本工作。代码使用起来比较复杂,为了去完美适配该机制,你必须要将所需的所有敏感权限都去像这样处理一次。
想跳楼的话就去吧,我不拦你...
处理 "Never Ask Again"
如果用户第一次拒绝了一个权限,在第二次申请该权限时会出现“Nerver ask again”的选项,这个选项作用是用来以后不再显示申请该权限的弹窗。
如果用户在点击拒绝前勾选了这个选项,那么下次我们在调用 requestPermissions
的时候就不会弹出申请权限的Dialog 了,然后该权限对应的功能将不会被执行。
但是从交互上来说,如果用户的操作没有任何反馈,体验是相当差的,所以这种情况必须有所处理才行。在调用 requestPermissions
之前,我们需要通过Activity的shouldShowRequestPermissionRationale方法来判断是否需要弹出一个用于解释该权限用途的Dialog,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
final
private
int
REQUEST_CODE_ASK_PERMISSIONS =
123
;
private
void
insertDummyContactWrapper() {
int
hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
if
(hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
if
(!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS)) {
showMessageOKCancel(
"You need to allow access to Contacts"
,
new
DialogInterface.OnClickListener() {
@Override
public
void
onClick(DialogInterface dialog,
int
which) {
requestPermissions(
new
String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
}
});
return
;
}
requestPermissions(
new
String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return
;
}
insertDummyContact();
}
private
void
showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new
AlertDialog.Builder(MainActivity.
this
)
.setMessage(message)
.setPositiveButton(
"OK"
, okListener)
.setNegativeButton(
"Cancel"
,
null
)
.create()
.show();
}
|
这样做的结果就是,当这个权限第一次被申请或者用户曾经勾选了“Never ask again”时弹出一个解释性的Dialog,然后用户拒绝权限时再使用该权限相关功能的时候,onRequestPermissionsResult里会直接返回PERMISSION_DENIED
标识,并且会弹出一个尚未授权的弹窗
所有流程搞定!
一次性申请多条权限
有些功能需要涉及到多个权限时,你可以用上述方法一次性将所有权限都申请下来,当然,不要忘了处理每条权限下对应的“Nerver ask again”选项。
修改后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
final
private
int
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS =
124
;
private
void
insertDummyContactWrapper() {
List<String> permissionsNeeded =
new
ArrayList<String>();
final
List<String> permissionsList =
new
ArrayList<String>();
if
(!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
permissionsNeeded.add(
"GPS"
);
if
(!addPermission(permissionsList, Manifest.permission.READ_CONTACTS))
permissionsNeeded.add(
"Read Contacts"
);
if
(!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS))
permissionsNeeded.add(
"Write Contacts"
);
if
(permissionsList.size() >
0
) {
if
(permissionsNeeded.size() >
0
) {
// Need Rationale
String message =
"You need to grant access to "
+ permissionsNeeded.get(
0
);
for
(
int
i =
1
; i < permissionsNeeded.size(); i++)
message = message +
", "
+ permissionsNeeded.get(i);
showMessageOKCancel(message,
new
DialogInterface.OnClickListener() {
@Override
public
void
onClick(DialogInterface dialog,
int
which) {
requestPermissions(permissionsList.toArray(
new
String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
}
});
return
;
}
requestPermissions(permissionsList.toArray(
new
String[permissionsList.size()]),
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
return
;
}
insertDummyContact();
}
private
boolean
addPermission(List<String> permissionsList, String permission) {
if
(checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
permissionsList.add(permission);
// Check for Rationale Option
if
(!shouldShowRequestPermissionRationale(permission))
return
false
;
}
return
true
;
}
|
每条权限的申请结果都会触发在同一个回调方法 onRequestPermissionsResult
. 我使用了HashMap来使代码看起来更清爽和更具可读性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@Override
public
void
onRequestPermissionsResult(
int
requestCode, String[] permissions,
int
[] grantResults) {
switch
(requestCode) {
case
REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
{
Map<String, Integer> perms =
new
HashMap<String, Integer>();
// Initial
perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);
// Fill with results
for
(
int
i =
0
; i < permissions.length; i++)
perms.put(permissions[i], grantResults[i]);
// Check for ACCESS_FINE_LOCATION
if
(perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
&& perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
// All Permissions Granted
insertDummyContact();
}
else
{
// Permission Denied
Toast.makeText(MainActivity.
this
,
"Some Permission is Denied"
, Toast.LENGTH_SHORT)
.show();
}
}
break
;
default
:
super
.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
|
这种业务情况是灵活可变的,你需要自己决定如何去做。某些情况下,如果没有任何权限被允许,则该功能就无法使用,而某些情况下,该功能仍然可以使用,但是会存在一些限制,我无法给出建议,这取决于你怎么去设计。
使用Support包去向前兼容
虽然上述代码在Android6.0棉花糖上能完美运行,但不幸的是,在棉花糖之前版本会导致崩溃,因为这些方法是在API23才添加进来的。
你可以用以下代码来检查当前版本号进行处理:
1
2
3
4
5
|
if
(Build.VERSION.SDK_INT >=
23
) {
// Marshmallow+
}
else
{
// Pre-Marshmallow
}
|
但是这样的代码显得过于复杂。所以我建议你使用Support Library v4,这个包已经为这种情况做了适配。将以下代码替换进功能中:
- ContextCompat.checkSelfPermission()
无论程序是否运行在M版本,对应的功能如果被授权成功后都会返回PERMISSION_GRANTED
,否则返回 PERMISSION_DENIED
。
- ActivityCompat.requestPermissions()
如果在M版本之前的系统上调用,OnRequestPermissionsResultCallback 方法会直接返回 PERMISSION_GRANTED
或 PERMISSION_DENIED
- ActivityCompat.shouldShowRequestPermissionRationale()
如果在M版本之前的系统上调用,总是返回false
。
最好所有场景下都用Support Library v4里的checkSelfPermission
, requestPermissions和
shouldShowRequestPermissionRationale
来替换Activity 里的这些方法,这样才能保证在相同的代码逻辑下你的程序你能在任何Andorid设备上都保持相同的表现。需要注意的是,这些方法需要一些额外的参数,例如Context或Activity,只需要正确的传入即可,不需要做额外工作。下面是示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
private
void
insertDummyContactWrapper() {
int
hasWriteContactsPermission = ContextCompat.checkSelfPermission(MainActivity.
this
,
Manifest.permission.WRITE_CONTACTS);
if
(hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
if
(!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.
this
,
Manifest.permission.WRITE_CONTACTS)) {
showMessageOKCancel(
"You need to allow access to Contacts"
,
new
DialogInterface.OnClickListener() {
@Override
public
void
onClick(DialogInterface dialog,
int
which) {
ActivityCompat.requestPermissions(MainActivity.
this
,
new
String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
}
});
return
;
}
ActivityCompat.requestPermissions(MainActivity.
this
,
new
String[] {Manifest.permission.WRITE_CONTACTS},
REQUEST_CODE_ASK_PERMISSIONS);
return
;
}
insertDummyContact();
}
|
Support v4的这些方法也适用于Fragment,所以可以随意将其添加进Fragment。
利用第三方库简化代码
你应该注意到这些代码有点复杂,不出意料,现在已经有很多第三方库致力于解决这些问题,我体验了很多这样的库后发现了一个符合我胃口的库,它就是hotchemi 的 PermissionsDispatcher.
这个库用更简洁更清晰的代码完成了上面我描述的所有操作,并且保证了灵活性,你可以自行测试下该库是否能适用于你的应用程序,如果不行的话,你还是能通过我介绍的方式进行适配操作。
如果运行中的程序被撤消掉某个权限会发生什么样的情况?
正如上面提到的,用户可以通过设置面板随时撤消掉某个权限。
那么当程序正在运行时撤消掉某个权限会发生什么样的事呢?经过我的测试后发现当程序的权限被撤消后会立马停止运行(不是崩溃),程序内所有线程都会直接停止运行。这对我来说应该是好事,因为如果操作系统允许程序继续运行,它可能带来Freddy一般的噩梦(出自电影《惊声尖叫》),我觉得没有比这更恐怖的了...
总结和建议
我相信你已经对这个新的权限系统有了很清晰的了解,同时我相信你已经知道了问题的严重性。
但是你没得选择,运行时权限已经在Android棉花糖上使用了,只能和它对着肛(→ܫ←)!我们唯一能做的就是赶紧立刻马上去适配这个新机制。
好消息是只有一小部分权限是需要申请使用的,大多数常用的权限,例如网络权限INTERNET,都是普通权限,这些权限在软件安装的会默认被授权,你不需要针对这些权限做额外操作。简而言之,你需要修改一些代码去适配它。
下面是两条建议:
1) 要把新的运行时权限机制当成一件紧急的事情来看待(反正就要是认真对待啦)
2) 如果你的应用还没完美适配运行时权限机制,千万不要手贱把targetSdkVersion设置为23!!!尤其是当你在Android Studio上新建项目的时候,别忘了去检查下build.gradle里的targetSdkVersion!
谈到代码的修改工作,我必须承认这确实是个大工程,如果之前代码架构做的不好,那你估计要花一些时间去认真重构,至少我相信每个应用程序都需要一些重构,正如我上面所说的,你没得选择~
现在看来,权限的概念已经发生了颠覆性的改变,现在如果程序的某些权限没有被授权,那么你的程序内功能就会受到很多限制。所以我建议你把所需的权限全部列出来,考虑各种可能的情况:例如当权限A被授权,但是权限B被拒绝了会怎么样,诸如此类的问题...
祝你重构顺利!认真对待,并把它列在你的计划表里,当Android M正式发布的时候你就高枕无忧了。
希望你能从这篇文章中有所收获。