全部开发者教程

Android 入门教程

菜单类控件
菜单:Menu
并发编程

内容提供者 - Content Provider

本节学习最后一个 Android 组件——内容提供者。顾名思义,它可以用来给其他的 App 提供各种内容,比如 Android 自带的短信、联系人、日历等等都是一个普通的 App,当你需要这些内容的时候,就可以向它们的 Content Provider 发起请求,然后拿到相应的数据。

1. 内容提供者的定义

照旧,首先看看官方解释:

Content providers are one of the primary building blocks of Android applications, providing content to applications. They encapsulate data and provide it to applications through the single ContentResolver interface. A content provider is only required if you need to share data between multiple applications. For example, the contacts data is used by multiple applications and must be stored in a content provider. If you don’t need to share data amongst multiple applications you can use a database directly via android.database.sqlite.SQLiteDatabase.
When a request is made via a ContentResolver the system inspects the authority of the given URI and passes the request to the content provider registered with the authority. The content provider can interpret the rest of the URI however it wants. The UriMatcher class is helpful for parsing URIs.

文档解释略长,这里用自己的话简要描述一发:

Content Provider 是 Android 四大组件之一,通过它可以向其他 App 提供数据。当收到其他 App 的数据请求时,会有一个 Content Resolver 接口统一对请求进行处理。数据请求通过 URI 的形式发起,每一次请求都需要带上 URI,Content Resolver 非常灵活并且可控性很强,在这里我们可以对来访者做鉴权,然后根据权限的不同给予不同敏感级别的数据,甚至可以对部分 App 拒绝提供数据。Content Provider 对数据的管理方式并不关心,可以采用 DataBase、文件、远程服务端等等 Android 支持的任何一种形式,整个工作流程如下:

ContentProvider

2. 为什么需要内容提供者

首先我们聊聊 Content Provider 存在的意义,在 Android 中每个 App 运行于一个独立的进程,而不同进程之间一般是无法直接通信的,所以一个 App 里的数据不能直接共享给其他 App 使用,这就会让很多功能难以实现。
在 Android 系统中我们可以很简单的创建 DataBase 来管理我们的数据,但是出于安全性的考虑,DataBase 只能在 App 内部使用,App 之间是无法共享内存的。因此,为了能够方便的将自己的数据共享出去,Android 系统引入了 Content Provider 组件,让我们可以和其他的 App 很方便的进行数据交换,甚至可以用来做 IPC(进程间通信),这就是内容提供者存在的意义。

不适合的使用场景:
当你使用了内容提供者,就表示你的数据是对外暴露的,所以这是一个相对敏感的操作,很多 App 的漏洞都是由于 Content Provider 使用不当导致的,所以这里需要多加注意。
Content Provider 不仅仅可以很方便的对其他 App 提供数据,当然也可以给 App 内部其他模块、或者 App 的其他进程提供数据,在多人、多业务合作的场景下使用 Content Provider 确实很便利。但是要注意的是,如果共享的数据仅仅是一个 App 私有的数据,最好不要使用 Content Provider。

3. Content Provider 相关概念

在编写 Content Provider 之前,我们先来熟悉几个概念:

3.1 Content URI

Content URI 可以用来让 provider 唯一标识一个数据,它包括四个部分:

  • Scheme: Content Provider 的 Scheme 是固定的字符串——“content”
  • Authority: provider 的唯一标识,我们通过“Authority”字段来从众多的 Content Provider 中找到我们想要的那个。
  • Path: path 字段帮助我们描述出我们想要的最具体的数据。比如我们想要查询通话记录,可以通过不同的 path 来查询具体的“未接来电”、“播出来电”、“已接来电”等等。
  • ID: 数字类型的可选字段,在一些特殊场景下可以使用 ID 来区分不同的类别。

3.2 身份鉴权

在 Content Resolver 中使用对请求者的身份进行鉴权,通过 URI 中的 Authority 字段我们可以区分出不同的请求者。然后根据请求者的 path 字段和 ID 字段(如果有 ID)我们能够精准的知道它想要的数据,最后可以根据它的权限等级来给它提供相应的数据。

Authority

4. Content Provider 的常用操作

常用操作基本上就是下面四种,和数据库非常类似:

  1. 查询(Querying):
    查询某个 Content Provider 支持的所有数据对象
  2. 删除(Delete):
    从 Content Provider 的数据库中删除具体的数据对象
  3. 更新(Update):
    更新数据对象
  4. 插入(Insert):
    插入一个新的数据对象
  5. onCreate():
    在 provider 被创建的时候回调

Operation

5. Content Provider 使用示例

和 Broadcast 类似,Android 系统也为我们预置了很多必备的 Content Provider,我们来学习一下如何使用。

5.1 读取短信收件箱

Android 系统中短信也是一个 App,为了方便其他 App 读取短信(比如接收验证码),短信提供了一个 Content Provider 接口供我们使用,当然读取短信属于敏感操作,必须添加以下权限:

    <uses-permission android:name="android.permission.READ_SMS"/>

在 Android 6.0 以前只需要静态注册权限即可,但是这个年代应该没有多少 Android 6.0 以下的机型了。在 6.0 之后我们还需要在代码中动态申请权限,然后通过content://sms/来查询短信消息,代码如下:


package com.emercy.myapplication;

import android.Manifest;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

public class MainActivity extends Activity implements View.OnClickListener {

    private static final String TAG = "ContentProvider";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.get_sms).setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            int hasReadSmsPermission = checkSelfPermission(Manifest.permission.READ_SMS);
            if (hasReadSmsPermission != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{Manifest.permission.READ_SMS}, 100);
                return;
            }
        }

        Uri uri = Uri.parse("content://sms/");
        ContentResolver resolver = getContentResolver();
        //获取的是哪些列的信息
        Cursor cursor = resolver.query(uri, new String[]{"address", "date", "type", "body"}, null, null, null);
        while (cursor.moveToNext()) {
            String address = cursor.getString(0);
            String date = cursor.getString(1);
            String type = cursor.getString(2);
            String body = cursor.getString(3);
            Log.d(TAG, "地址:" + address);
            Log.d(TAG, "时间:" + date);
            Log.d(TAG, "类型:" + type);
            Log.d(TAG, "内容:" + body);
        }
        cursor.close();
    }
}

contentView 的布局代码就不贴了,只需要一个 Button 即可。在 onClick 方法中我们首先申请权限,此时手机会弹出一个权限申请的弹窗,入剩下:
sms_permision
点击同意之后就可以观察 Logcat 了,过滤 Tag 为 ContentProvider,结果如下:

get_sms

这样就能够顺利读取出短信相关的信息了。

5.2 读取联系人

其他的代码和读取短信一样,修改的部分就是onClick()中的部分,主要是权限申请和数据读取,修改 onClick() 中的代码如下:

if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
            int hasReadSmsPermission = checkSelfPermission(Manifest.permission.READ_CONTACTS);
            if (hasReadSmsPermission != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, 100);
                return;
            }
}

ContentResolver resolver = getContentResolver();
Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
Cursor cursor = resolver.query(uri, null, null, null, null);
while (cursor.moveToNext()) {
            String cName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
            String cNum = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
            Log.d(TAG, "姓名:" + cName);
            Log.d(TAG, "号码:" + cNum);
        }
cursor.close();

然后在 AndroidManifest.xml 中加入读取联系人的权限:

<uses-permission android:name="android.permission.READ_CONTACTS"/>

第一次点击的时候同样会弹出以下权限申请弹出,授予之后即可拿到具体的联系人信息。

contact_permission

掌握了这两种 Content Provider 的使用,其余的像新增联系人、查询具体联系人等等其实都是换汤不换药,核心思路就是两步:1、申请权限(Android 6.0 以上需要动态申请);2、通过 URI 读取数据。

6. 小结

本节学习最后一个 Android 组件,它的功能是为其他 App 提供相关的数据,让数据可以在不同 App、不同进程之间相互共享,但是要注意的是它的适用场景,一定只是在向其他 App 输出数据的时候用,为了避免数据泄露出现安全风险,其他场景要慎用。使用 Content Provider 首先关注一下改数据是否需要申请权限,然后查询到该数据的 URI,通过拼接 URI 就可以完成数据的获取了。