内容提供器
在上一章中我们学了 Android 数据持久化的技术, 包括文件存储、SharedPreferences 存储以及数据库存储. 使用这些持久化技术所保存的数据都只能在当前应用程序中访问.
虽然文件和 SharedPreferences 存储中提供了 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE 这两种操作模式, 用于供给其他的应用程序访问当前应用的数据, 但这两种模式在 Android 4.2 版本中都已被废弃了. 为什么呢? 因为 Android 官方已经不再推荐使用这种方式来实现跨程序数据共享的功能, 而是应该使用更加安全可靠的内容提供器技术.
为什么要将我们程序中的数据共享给其他程序呢?
当然, 这个是要视情况而定的, 比如说账号和密码这样的隐私数据显然是不能共享给其他程序的, 不过一些可以让其他程序进行二次开发的基础性数据, 我们还是可以选择将其共享的. 例如系统的电话簿程序, 它的数据库中保存了很多的联系人信息, 如果这些数据都不允许第三方的程序进行访问的话, 恐怕很多应用的功能都要大打折扣了. 除了电话簿之外, 还有短信、媒体库等程序都实现了跨程序数据共享的功能, 而使用的技术当然就是内容提供器了, 下面我们就来对这一技术进行深入的探讨.
内容提供器简介
内容提供器 (Content Provider) 主要用于在不同的应用程序之间实现数据共享的功能, 它提供了一套完整的机制, 允许一个程序访问另一个程序中的数据, 同时还能保证被访数据的安全性.
目前, 使用内容提供器是 Android 实现跨程序共享数据的标准方式. 不同于文件存储和 SharedPreferences 存储中的两种全局可读写操作模式, 内容提供器可以选择只对哪一部分数据进行共享, 从而保证我们程序中的隐私数据不会有泄漏的风险.
不过在正式开始学习内容提供器之前, 我们需要先掌握另外一个非常重要的知识 —— Android 运行时权限, 因为待会的内容提供器示例中会使用到运行时权限的功能. 当然不光是内容提供器, 以后我们的开发过程中也会经常使用到运行时权限, 因此你必须能够牢牢掌握它才行.
运行时权限
Android 的权限机制并不是什么新鲜事物, 从系统的第一个版本开始就已经存在了. 但其实之前 Android 的权限机制在保护用户安全和隐私等方面起到的作用比较有限, 尤其是一些大家都离不开的常用软件, 非常容易 "店大欺客". 为此, Android 开发团队在 Android 6.0 系统中引用了运行时权限这个功能, 从而更好地保护了用户的安全和隐私, 那么本节我们就来详细学习一下这个 6.0 系统中引入的新特性.
Android 权限机制详解
首先来回顾一下过去 Android 的权限机制是什么样的. 在第 5 章写 BroadcastTest 项目的时候第一次接触了 Android 权限相关的内容, 当时为了要访问系统的网络状态以及监听开机广播, 于是在 AndroidManifest.xml 文件中添加了这样两句权限声明:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.broadcasttest">
<!-- 下面两行是权限声明 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.BroadcastTest">
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exported="true">
<intent-filter android:priority="100">
<action android:name="com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
因为访问系统的网络状态以及监听开机广播涉及了用户设备的安全性, 因此必须在 AndroidManifest.xml 中加入权限声明, 否则我们的程序就会崩溃.
加入了这两句权限声明后, 对于用户来说到底有什么影响呢? 为什么这样就可以保护用户设备的安全性了呢?
其实用户主要在以下两个方面得到了保护, 一方面, 如果用户在低于 6.0 系统的设备上安装该程序, 会在安装界面给出下图所示的提醒. 这样用户就可以清楚地知晓该程序一共申请了哪些权限, 从而决定是否要安装这个程序.
另一方面, 用户可以随时在应用程序管理界面查看任意一个程序的权限申请情况, 如下图所示. 这样该程序申请的所有权限就尽收眼底, 什么都瞒不过用户的眼睛, 以此保证应用程序不会出现各种滥用权限的情况.
这种权限机制的设计思路其实非常简单, 就是用户如果认可你所申请的权限, 那么就会安装你的程序, 如果不认可你所申请的权限, 那么拒绝安装就可以了.
但是理想是美好的, 现实却很残酷, 因为很多我们所离不开的常用软件普遍存在着滥用权限的情况, 不管到底用不用得到, 反正先把权限申请了再说. 比如说微信所申请的权限列表如图所示:
这只是微信所申请的一半左右的权限, 因为权限太多一屏截不下来. 其中有一些权限我并不认可, 比如微信为什么要读取我手机的短信和彩信? 但是我不认可又能怎样, 难道我拒绝安装微信? 没错, 这种例子比比皆是, 当一些软件已经让我们产生依赖的时候就会容易 "店大欺客", 反正这个权限我就是要了, 你自己看着办吧!
Android 开发团队当然也意识到了这个问题, 于是在 6.0 系统中加入了运行时权限功能. 也就是说, 用户不需要在安装软件的时候一次性授权所有申请的权限, 而是可以在软件的使用过程中再对某一项权限申请进行授权. 比如说一款相机应用在运行时申请了地理位置定位权限, 就算我拒绝了这个权限, 但是我应该仍然可以使用这个应用的其他功能, 而不是像之前那样直接无法安装它.
当然, 并不是所有权限都需要在运行时申请, 对于用户来说, 不停地授权也很烦琐.
Android 现在将所有的权限归成了两类, 一类是普通权限, 一类是危险权限. 准确地讲, 其实还有第三类特殊权限, 不过这种权限使用得很少, 因此不在本书的讨论范围之内.
普通权限指的是那些不会直接威胁到用户的安全和隐私的权限, 对于这部分权限申请, 系统会自动帮我们进行授权, 而不需要用户再去手动操作了, 比如在 BroadcastTest 项目中申请的两个权限就是普通权限.
危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限, 如获取设备联系人信息、定位设备的地理位置等, 对于这部分权限申请, 必须要由用户手动点击授权才可以, 否则程序就无法使用相应的功能.
但是, Android 中有一共有上百种权限, 我们怎么从中区分哪些是普通权限, 哪些是危险权限呢? 其实并没有那么难, 因为危险权限总共就那么几个, 除了危险权限之外, 剩余的就都是普通权限了. 下表列出了 Android 中所有的危险权限, 一共是 9 组 24 个权限.
这张表格你看起来可能并不会那么轻松, 因为里面的权限全都是你没使用过的. 不过没有关系, 你并不需要了解表格中每个权限的作用, 只要把它当成一个参照表来查看就行了. 每当要使用一个权限时, 可以先到这张表中来查一下, 如果是属于这张表中的权限, 那么就需要进行运行时权限处理, 如果不在这张表中, 那么只需要在 AndroidManifest.xml 文件中添加一下权限声明就可以了.
另外注意一下, 表格中每个危险权限都属于一个权限组, 我们在进行运行时权限处理时使用的是权限名, 但是用户一旦同意授权了, 那么该权限所对应的权限组中所有的其他权限也会同时被授权.
访问: http://developer.android.google.cn/reference/android/Manifest.permission.html 可以查看 Android 系统中完整的权限列表.
好了, 关于 Android 权限机制的内容就讲这么多, 理论知识你已经了解得非常充足了. 接下来我们就学习一下到底如何在程序运行的时候申请权限.
在程序运行时申请权限
新建一个 RuntimePermissionTest 项目, 在这个项目的基础上来学习运行时权限的使用方法. 上表中列出的所有权限都是可以申请的, 简单起见就使用 CALL_PHONE
这个权限来作为本小节中的示例. CALL_PHONE
这个权限是编写拨打电话功能的时候需要声明的, 因为拨打电话会涉及用户手机的资费问题, 因而被列为了危险权限.
在 Android 6.0 系统出现之前, 拨打电话功能的实现其实非常简单, 修改 activity_main.xml
布局文件, 如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/make_call"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Make Call">
</Button>
</LinearLayout>
上面的在布局文件中只是定义了一个按钮, 当点击按钮时就去触发拨打电话的逻辑. 接着修改 MainActivity.java 中的代码, 如下所示:
package com.example.runtimepermissiontest;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
} catch (SecurityException e) {
e.printStackTrace();
}
}
});
}
}
在按钮的点击事件中, 构建了一个隐式 Intent , Intent 的 action 指定为 Intent.ACTION_ CALL
, 这是一个系统内置的打电话的动作, 然后在 data 部分指定了协议是 tel, 号码是 10086.
在 2.3.3 小节中就已经见过了, 当时指定的 action 是 Intent.ACTION_DIAL
, 表示打开拨号界面, 这个是不需要声明权限的, 而 Intent.ACTION_ CALL
则可以直接拨打电话, 因此必须声明权限.
另外为了防止程序崩溃, 我们将所有操作都放在了异常捕获代码块当中. 那么接下来修改 AndroidManifest.xml 文件, 在其中声明如下权限:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.runtimepermissiontest">
<!-- 添加权限声明 -->
<uses-permission android:name="android.permission.CALL_PHONE"></uses-permission>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.RuntimePermissionTest">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
这样我们就将拨打电话的功能成功实现了, 并且在低于 Android 6.0 系统的手机上都是可以正常运行的, 但是如果我们在 6.0 或者更高版本系统的手机上运行, 点击 Make Call 按钮就没有任何效果, 这时观察 logcat 中的打印日志, 你会看到如图:
错误信息中提醒我们 "Permission Denial", 可以看出, 是由于权限被禁止所导致的, 因为 6.0 及以上系统在使用危险权限时都必须进行运行时权限处理. 那么下面我们就来尝试修复这个问题, 修改 MainActivity.java 中的代码, 如下所示:
package com.example.runtimepermissiontest;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button makeCall = (Button) findViewById(R.id.make_call);
makeCall.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
// 调用 ActivityCompat.requestPermissions() 方法向用户申请权限
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE}, 1);
} else {
// 拨打电话
call();
}
}
});
}
// 拨打电话
private void call() {
try {
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
} catch (SecurityException e) {
e.printStackTrace();
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case 1:
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
call();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
}
上面的代码将运行时权限的完整流程都覆盖了, 下面我们来具体解析一下.
运行时权限的核心就是在程序运行过程中由用户授权我们去执行某些危险操作, 程序是不可以擅自做主去执行这些危险操作的.
因此, 第一步就是要先判断用户是不是已经给过我们授权了. 借助的是 ContextCompat.checkSelfPermission()
方法. checkSelfPermission()
方法接收两个参数, 第一个参数是 Context, 第二个参数是具体的权限名, 比如打电话的权限名就是 Manifest.permission.CALL_PHONE
, 然后我们使用方法的返回值和 PackageManager. PERMISSION_GRANTED
做比较, 相等就说明用户已经授权, 不等就表示用户没有授权.
如果已经授权的话就简单了, 直接去执行拨打电话的逻辑操作就可以了, 这里我们把拨打电话的逻辑封装到了 call()
方法当中.
如果没有授权的话, 则需要调用 ActivityCompat.requestPermissions()
方法来向用户申请授权, requestPermissions()
方法接收 3 个参数, 第一个参数要求是 Activity 的实例, 第二个参数是一个 String 数组, 我们把要申请的权限名放在数组中即可, 第三个参数是请求码, 只要是唯一值就可以了, 这里传入了 1.
调用完了 requestPermissions()
方法之后, 系统会弹出一个权限申请的对话框, 然后用户可以选择同意或拒绝我们的权限申请, 不论是哪种结果, 最终都会回调到 onRequestPermissionsResult()
方法中, 而授权的结果则会封装在 grantResults 参数当中. 这里我们只需要判断一下最后的授权结果, 如果用户同意的话就调用 call()
方法来拨打电话, 如果用户拒绝的话我们只能放弃操作, 并且弹出一条失败提示.
现在重新运行一下程序, 并点击 Make Call 按钮, 效果如图:
没开飞行模式时:
开着飞行模式时:
由于用户还没有授权过我们拨打电话权限, 因此第一次运行会弹出这样一个权限申请的对话框, 用户可以选择同意或者拒绝.
可以看到, 这次我们就成功进入到拨打电话界面了, 并且由于用户已经完成了授权操作, 之后再点击 Make Call 按钮就不会再弹出权限申请对话框了, 而是可以直接拨打电话.
用户随时都可以将授予程序的危险权限进行关闭, 进入 Settings → Apps → RuntimePermissionTest → Permissions, 在这里我们就可以对任何授予过的危险权限进行关闭了, 界面如图:
关闭权限之后, 再回到 RuntimePermissionTest 应用, 点击 Make Call 按钮的效果:
这次第一次点, 提示用户选择, 如果选择了拒绝, 再点时则不会再弹出弹窗询问用户了.
好了, 关于运行时权限的内容就讲到这里, 现在你已经有能力处理 Android 上各种关于权限的问题了, 下面我们就来进人本章的正题 -- 内容提供器.
访问其他程序中的数据
内容提供器的用法一般有两种:
-
一种是使用现有的内容提供器来读取和操作相应程序中的数据.
-
一种是创建自己的内容提供器给我们程序的数据提供外部访问接口.
使用现有的内容提供器: 如果一个应用程序通过内容提供器对其数据提供了外部访问接口, 那么任何其他的应用程序就都可以对这部分数据进行访问.
Android 系统中自带的电话簿、短信、媒体库等程序都提供了类似的访问接口, 这就使得第三方应用程序可以充分地利用这部分数据来实现更好的功能. 下面我们就来看一看, 内容提供器到底是如何使用的.
ContentResolver 的基本用法
对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助 ContentResolver 类,可以通过 Context 中的 getContentResolver()
方法获取到该类的实例。
ContentResolver 中提供了一系列的方法用于对数据进行 CRUD 操作,其中 insert()
方法用于添加数据,update()
方法用于更新数据,delete()
方法用于删除数据,query()
方法用于查询数据。
不同于 SQLiteDatabase, ContentResolver 中的增删改查方法都是不接收表名参数的,而是使用一个 Uri 参数代替,这个参数被称为内容 URI。
内容 URI 给内容提供器中的数据建立了唯一标识符,它主要由两部分组成:authority 和 path。
authority 是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是 com.example.app
,那么该程序对应的 authority 就可以命名为 com.example.app.provider
。
path 则是用于对同一应用程序中不同的表做区分的,通常都会添加到 authority 的后面。比如某个程序的数据库里存在两张表:table1 和 table2,这时就可以将 path 分别命名为 /table1
和 /table2
,然后把 authority 和 path 进行组合,内容 URI 就变成了 com.example.app.provider/table1
和 com.example.app.provider/table2
。
不过,目前还很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。因此,内容URI最标准的格式写法如下:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
可以看出来,内容 URI 可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据。也正是因此,ContentResolver 中的增删改查方法才都接收 Uri 对象作为参数,因为如果使用表名的话,系统将无法得知我们期望访问的是哪个应用程序里的表。
在得到了内容 URI 字符串之后,我们还需要将它解析成 Uri 对象才可以作为参数传入。解析的方法也相当简单,代码如下所示:
Uri uri = Uri.parse("content://com.example.app.provider/table1")
只需要调用 Uri.parse()
方法,就可以将内容 URI 字符串解析成 Uri 对象了。