1. 项目概述
在安卓应用开发中,权限管理是一个绕不开的核心话题。特别是从Android 6.0(API 23)开始引入的运行时权限机制,彻底改变了开发者处理敏感权限的方式。这个机制要求应用在运行时动态请求某些涉及用户隐私的权限,而不是像以前那样在安装时一次性获取所有权限。
作为安卓四大组件之一的内容提供者(Content Provider),经常需要访问用户的联系人、短信、通话记录等敏感数据。这些操作都涉及到危险权限(Dangerous Permission),必须通过运行时权限机制来获取用户授权。如果处理不当,轻则功能无法正常使用,重则导致应用崩溃或被应用商店下架。
2. 运行时权限机制详解
2.1 权限分类与变化
在Android系统中,权限分为两大类:
-
普通权限(Normal Permission):不会直接威胁用户隐私或设备操作的权限,如设置时区、访问网络状态等。这些权限在AndroidManifest.xml中声明后系统会自动授予。
-
危险权限(Dangerous Permission):涉及用户隐私数据或可能影响设备操作的权限,如读取联系人、访问精确位置等。这些权限必须通过运行时动态申请。
特别值得注意的是,权限系统在Android 10(API 29)和Android 11(API 30)又有重要更新:
- Android 10引入了分区存储(Scoped Storage),进一步限制了对共享存储的访问
- Android 11调整了权限对话框的显示逻辑,增加了"仅限这一次"的选项
2.2 权限组概念
Android将危险权限按功能分组管理,称为权限组(Permission Group)。例如:
- CONTACTS组包含READ_CONTACTS和WRITE_CONTACTS
- LOCATION组包含ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION
当应用请求某个权限组中的任一权限时:
- 如果用户尚未授予该组任何权限,系统会显示权限请求对话框
- 如果用户已授予该组某个权限,系统会立即授予同组的其他权限(无需再次提示)
注意:从Android 8.0开始,系统会单独提示每个权限请求,不再自动授予同组权限
3. 内容提供者的权限需求
3.1 常见需要权限的内容提供者操作
内容提供者经常需要访问以下系统数据,这些操作都需要相应权限:
-
联系人数据
- 读取联系人:READ_CONTACTS
- 修改联系人:WRITE_CONTACTS
-
通话记录
- 读取通话记录:READ_CALL_LOG
- 写入通话记录:WRITE_CALL_LOG
-
短信相关
- 读取短信:READ_SMS
- 发送短信:SEND_SMS
-
存储相关
- 读取外部存储:READ_EXTERNAL_STORAGE
- 写入外部存储:WRITE_EXTERNAL_STORAGE
3.2 权限声明方式
无论是否使用运行时权限,都需要先在AndroidManifest.xml中声明所需权限:
xml复制<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<application ...>
...
</application>
</manifest>
4. 运行时权限请求实现
4.1 基本请求流程
完整的运行时权限请求流程包括以下步骤:
- 检查是否已获得权限
- 如果未获得,检查是否需要显示权限请求说明
- 请求权限
- 处理权限请求结果
4.2 代码实现示例
以下是一个完整的权限请求实现示例:
java复制// 检查是否已有权限
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
// 是否需要显示权限请求说明
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.READ_CONTACTS)) {
// 显示解释对话框
showExplanationDialog();
} else {
// 直接请求权限
requestPermission();
}
} else {
// 已有权限,执行操作
queryContacts();
}
private void requestPermission() {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_CONTACTS},
PERMISSION_REQUEST_CODE);
}
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 权限已授予
queryContacts();
} else {
// 权限被拒绝
handlePermissionDenied();
}
}
}
4.3 权限请求最佳实践
-
按需请求:只在真正需要时才请求权限,不要一次性请求所有可能用到的权限
-
合理分组:将相关权限分组请求,但不要一次性请求太多权限组
-
提供解释:在请求前适当解释为什么需要这个权限,但不要过度解释
-
优雅降级:当权限被拒绝时,应用应能优雅降级而不是直接崩溃
-
处理"不再询问":当用户勾选"不再询问"后拒绝权限,应引导用户到设置中手动开启
5. 高级权限处理技巧
5.1 处理多个权限请求
当需要请求多个权限时,可以这样处理:
java复制String[] permissions = {
Manifest.permission.READ_CONTACTS,
Manifest.permission.WRITE_CONTACTS,
Manifest.permission.ACCESS_FINE_LOCATION
};
List<String> permissionsToRequest = new ArrayList<>();
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(permission);
}
}
if (!permissionsToRequest.isEmpty()) {
ActivityCompat.requestPermissions(this,
permissionsToRequest.toArray(new String[0]),
MULTI_PERMISSION_REQUEST_CODE);
}
然后在onRequestPermissionsResult中检查每个权限的授予情况。
5.2 权限请求说明对话框
当用户之前拒绝过权限请求时,应该显示一个解释对话框:
java复制private void showExplanationDialog() {
new AlertDialog.Builder(this)
.setTitle("需要联系人权限")
.setMessage("我们需要访问您的联系人以便...")
.setPositiveButton("确定", (dialog, which) -> requestPermission())
.setNegativeButton("取消", null)
.show();
}
5.3 处理"不再询问"的情况
当用户勾选"不再询问"并拒绝权限后,shouldShowRequestPermissionRationale会返回false。此时应该引导用户到设置页面:
java复制private void handlePermissionDenied() {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.READ_CONTACTS)) {
// 用户勾选了"不再询问"
showGoToSettingsDialog();
} else {
// 普通拒绝
showPermissionDeniedMessage();
}
}
private void showGoToSettingsDialog() {
new AlertDialog.Builder(this)
.setTitle("权限被永久拒绝")
.setMessage("您已永久拒绝联系人权限。如需使用此功能,请到设置中手动开启权限。")
.setPositiveButton("去设置", (dialog, which) -> openAppSettings())
.setNegativeButton("取消", null)
.show();
}
private void openAppSettings() {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
Uri uri = Uri.fromParts("package", getPackageName(), null);
intent.setData(uri);
startActivity(intent);
}
6. 内容提供者权限实战
6.1 查询联系人示例
在获取READ_CONTACTS权限后,可以通过ContentResolver查询联系人:
java复制private void queryContacts() {
ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.query(
ContactsContract.Contacts.CONTENT_URI,
null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
do {
String name = cursor.getString(
cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
Log.d("Contacts", "Name: " + name);
} while (cursor.moveToNext());
cursor.close();
}
}
6.2 添加联系人示例
添加联系人需要WRITE_CONTACTS权限:
java复制private void addContact(String name, String phone) {
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
// 添加原始联系人
ops.add(ContentProviderOperation.newInsert(
ContactsContract.RawContacts.CONTENT_URI)
.withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
.withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null)
.build());
// 添加姓名
ops.add(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, name)
.build());
// 添加电话号码
ops.add(ContentProviderOperation.newInsert(
ContactsContract.Data.CONTENT_URI)
.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
.withValue(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)
.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, phone)
.withValue(ContactsContract.CommonDataKinds.Phone.TYPE,
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE)
.build());
try {
getContentResolver().applyBatch(ContactsContract.AUTHORITY, ops);
} catch (Exception e) {
e.printStackTrace();
}
}
7. 常见问题与解决方案
7.1 权限被拒绝后的处理
当权限被拒绝时,应用应该:
- 检查是否是永久拒绝(勾选了"不再询问")
- 如果是临时拒绝,可以在用户再次尝试相关功能时重新请求
- 如果是永久拒绝,引导用户到设置页面手动开启权限
- 提供没有该权限情况下的替代方案或功能降级
7.2 权限请求的最佳时机
请求权限的最佳实践:
- 上下文相关请求:在用户尝试使用需要权限的功能时请求,而不是在应用启动时
- 分批请求:不要一次性请求所有权限,按功能需要逐步请求
- 解释价值:在请求前简要说明权限的用途,但不要过度解释
7.3 测试权限相关代码
测试权限相关代码时需要注意:
- 测试各种权限授予/拒绝场景
- 测试"不再询问"的情况
- 测试权限被撤销的情况(用户可以在设置中随时撤销权限)
- 使用Android测试框架的GrantPermissionRule简化测试
java复制@RunWith(AndroidJUnit4.class)
public class ContactsTest {
@Rule
public GrantPermissionRule permissionRule = GrantPermissionRule.grant(
Manifest.permission.READ_CONTACTS);
@Test
public void testQueryContacts() {
// 测试代码
}
}
8. 兼容性考虑
8.1 不同Android版本的差异
- Android 5.1及以下:没有运行时权限概念,安装时授予所有权限
- Android 6.0-9.0:标准的运行时权限机制
- Android 10+:分区存储限制,权限细化
- Android 11+:一次性权限,"仅限这一次"选项
8.2 向后兼容实现
为了兼容旧版本Android,可以使用以下方法:
java复制if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// 使用运行时权限API
if (checkSelfPermission(permission) != PERMISSION_GRANTED) {
requestPermissions(new String[]{permission}, requestCode);
}
} else {
// 旧版本直接执行操作
doOperation();
}
8.3 权限库的使用
为了简化权限处理,可以使用第三方权限库,如:
- EasyPermissions:Google提供的简化权限处理的库
- RxPermissions:基于RxJava的权限请求库
- Dexter:简化权限请求流程的库
以EasyPermissions为例:
java复制@AfterPermissionGranted(RC_CONTACTS_PERM)
public void requestContactsPermission() {
String[] perms = {Manifest.permission.READ_CONTACTS};
if (EasyPermissions.hasPermissions(this, perms)) {
queryContacts();
} else {
EasyPermissions.requestPermissions(
this,
"需要联系人权限来显示您的联系人",
RC_CONTACTS_PERM,
perms);
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
EasyPermissions.onRequestPermissionsResult(
requestCode, permissions, grantResults, this);
}
9. 安全与隐私考虑
9.1 最小权限原则
- 只请求应用真正需要的权限
- 优先使用不需要权限的替代方案
- 及时释放不再需要的权限(通过撤销或不再使用相关功能)
9.2 数据安全处理
- 加密存储敏感数据
- 及时清除内存中的敏感数据
- 最小化数据收集范围
- 提供隐私政策说明
9.3 权限使用透明度
- 在隐私政策中明确说明权限用途
- 在应用描述中说明需要哪些权限及原因
- 在应用内适当位置解释权限用途
10. 实际开发中的经验分享
-
权限请求策略:不要一股脑地在应用启动时请求所有权限,而是应该在用户真正需要使用相关功能时才请求对应权限。这能显著提高权限通过率。
-
解释的艺术:权限请求前的解释对话框要简明扼要,重点说明权限能给用户带来什么价值,而不是技术细节。一个好的解释可以显著提高权限授予率。
-
降级体验:即使没有某些权限,应用也应该提供合理的降级体验。比如没有位置权限时可以手动选择城市,没有联系人权限时可以手动输入电话号码。
-
测试矩阵:建立完整的权限测试矩阵,覆盖各种权限组合和拒绝场景。特别注意测试权限被撤销后的应用行为。
-
权限监控:在应用的设置中添加权限状态监控页面,让用户可以一目了然地看到当前权限状态,并快速跳转到系统设置进行调整。
-
用户教育:对于关键权限,可以在首次拒绝后显示教育性提示,解释为什么这个权限对功能很重要,但不要过于频繁或强制。
-
权限回收:定期检查应用是否还在使用已获取的权限,对于长期不用的权限,考虑在代码中停止相关功能调用,减少应用的权限占用。