3.5.3 ContentProvider使用全解

本节例程下载地址:
WillFlowContentProvider
WillFlowProviderTest

一、内容提供器简介

内容提供程序管理一组共享的应用数据,用于在不同的应用程序之间实现数据共享的功能。它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。我们可以将数据存储在文件系统、SQLite 数据库、网络上或我们的应用可以访问的任何其他永久性存储位置。 其他应用可以通过内容提供程序查询数据,甚至修改数据(如果内容提供程序允许)。 例如,Android 系统可提供管理用户联系人信息的内容提供程序。 因此,任何具有适当权限的应用都可以查询内容提供程序的某一部分(如 ContactsContract.Data),以读取和写入有关特定人员的信息。

目前,使用内容提供器是 Android 实现跨程序共享数据的标准方式。不同于文件存储和 Shared Preferences 存储中的两种全局可读写操作模式,内容提供器可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。当然内容提供程序也适用于读取和写入我们的应用不共享的私有数据。 例如,记事本示例应用使用内容提供程序来保存笔记。

内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据,另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。这和我们之前学习的广播有点类似是吧?那么接下来我们就逐一开始学习。

二、访问其他程序中的数据

当一个应用程序通过内容提供器对其数据提供了外部访问接口,任何其他的应用程序就都可以对这部分数据进行访问。 Android 系统中自带的电话簿、短信、媒体库等程序都提供了类似的访问接口,这就使得第三方应用程序可以充分地利用这部分数据来实现更好的功能。下面我们就来看一看,内容提供器到底是如何使用的。

1、ContentResolver 的基本用法

对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助 ContentResolve 类,我们可以通过 Context 中的 getContentResolver() 方法获取到该类的实例。ContentResolver 中提供了一系列的方法用于对数据进行 CRUD 操作,其中 insert()方法用于添加数据, update()方法用于更新数据, delete()方法用于删除数据, query()方法用于查询数据。等我我们学习到后面,你会发现 SQLiteDatabase 中也是使用的这几个方法来进行 CRUD操作的,只不过它们在方法参数上稍微有一些区别。

不同于 SQLiteDatabase, ContentResolver 中的增删改查方法都是不接收表名参数的,而是使用一个 Uri 参数代替,这个参数被称为内容 URI。内容 URI 给内容提供器中的数据建立了唯一标识符,它主要由两部分组成,权限(authority)和路径(path)。

  • 权限是用于对不同的应用程序做区分的,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是 com.example.app,那么该程序对应的权限就可以命名为 com.example.app.provider。
  • 路径则是用于对同一应用程序中不同的表做区分的,通常都会添加到权限的后面。比如某个程序的数据库里存在两张表:table1 和 table2,这时就可以将路径分别命名为/table1和/table2,然后把权限和路径进行组合,内容 URI 就变成了 com.example.app.provider/table1和 com.example.app.provider/table2。

不过,目前还很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。因此,内容 URI 最标准的格式写法如下:

content://com.wgh.willflowcontentprovider.provider/table1/table1
content://com.wgh.willflowcontentprovider.provider/table1/table2

如果你足够细心的话你可以发现:内容 URI 可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据。 也正是因此, ContentResolver 中的增删改查方法才都接收 Uri 对象作为参数,因为使用表名的话系统将无法得知我们期望访问的是哪个应用程序里的表。

在得到了内容 URI 字符串之后,我们还需要将它解析成 Uri 对象才可以作为参数传入。解析的方法也相当简单,代码如下所示:

Uri uri = Uri.parse("content://com.wgh.willflowcontentprovider.provider/table1/table1")

只需要调用 Uri.parse()方法,就可以将内容 URI 字符串解析成 Uri 对象了。

现在我们就可以使用这个 Uri 对象来查询 table1 表中的数据了,代码如下所示:

    Cursor cursor = getContentResolver().query(
            uri,
            projection,
            selection,
            selectionArgs,
            sortOrder);

这些参数和 SQLiteDatabase 中 query() 方法里的参数很像,但总体来说要简单一些,毕竟这是在访问其他程序中的数据,没必要构建过于复杂的查询语句。下表对使用到的这部分参数进行了详细的解释。

query()方法参数 对应 SQL 部分 描述
uri from table_name 指定查询某个应用程序下的某一张表
projection select column1, column2 指定查询的列名
selection where column = value 指定 where 的约束条件
selectionArgs --- 为 where 中的占位符提供具体的值
orderBy order by column1, column2 指定查询结果的排序方式

查询完成后返回的仍然是一个 Cursor 对象,这时我们就可以将数据从 Cursor 对象中逐个读取出来了。读取的思路仍然是通过移动游标的位置来遍历 Cursor 的所有行,然后再取出每一行中相应列的数据,代码如下所示:

    if (cursor != null) {
        while (cursor.moveToNext()) {
            String column1 = cursor.getString(cursor.getColumnIndex("column1"));
            int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
        } 
        cursor.close();
    }

掌握了最难的查询操作,剩下的增加、修改、删除操作就更不在话下了。我们先来看看如何向 table1 表中添加一条数据,代码如下所示:

    ContentValues values = new ContentValues();
    values.put("column1", "text");
    values.put("column2", 1);
    getContentResolver().insert(uri, values);

可以看到,仍然是将待添加的数据组装到 ContentValues 中,然后调用 ContentResolver 的 insert() 方法,将 Uri 和 ContentValues 作为参数传入即可。

现在如果我们想要更新这条新添加的数据,把 column1 的值清空,可以借助 ContentResolver 的 update() 方法实现,代码如下所示:

    ContentValues values = new ContentValues();
    values.put("column1", "");
    getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[] {"text", "1"});

注意上述代码使用了 selection 和 selectionArgs 参数来对想要更新的数据进行约束,以防止所有的行都会受影响。

最后,可以调用 ContentResolver 的 delete()方法将这条数据删除掉,代码如下所示:

getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });

到这里为止,我们就把 ContentResolver 中的增删改查方法全部学完了,接下来,我们就利用目前所学的知识,看一看如何读取系统电话簿中的联系人信息。

2、读取系统联系人

由于我们之前一直使用的都是模拟器,电话簿里面并没有联系人存在,所以现在需要自己手动添加几个,以便稍后进行读取。打开电话簿程序,我们可以通过点击 Create a new contact 按钮来对联系人进行创建。这里就先创建两个联系人吧,分别填入他们的姓名和手机号,如图所示:


这样准备工作就做好了,首先还是来编写一下布局文件,这里我们希望读取出来的联系人信息能够在 ListView 中显示。

修改 activity_main.xml 中的代码,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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="com.wgh.willflowcontentprovider.MainActivity">

    <ListView
        android:id="@+id/contacts_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
接着修改 MainActivity 中的代码,如下所示:
public class MainActivity extends AppCompatActivity {
    ListView mContactsView;
    ArrayAdapter<String> mAdapter;
    List<String> mContactsList = new ArrayList<String>();

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

        mContactsView = (ListView) findViewById(R.id.contacts_view);
        mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mContactsList);
        mContactsView.setAdapter(mAdapter);
        readContacts();
    }

    private void readContacts() {
        Cursor cursor = null;
        try {// 查询联系人数据
            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
            while (cursor.moveToNext()) {// 获取联系人姓名
                String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));// 获取联系人手机号
                String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                mContactsList.add(displayName + "\n" + number);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }
}

在onCreate()方法中,我们首先获取了 ListView 控件的实例,并给它设置好了适配器,然后就去调用 readContacts()方法。下面重点看下 readContacts()方法,可以看到,这里使用了 ContentResolver 的 query()方法来查询系统的联系人数据。接着我们对 Cursor 对象进行遍历,将联系人姓名和手机号这些数据逐个取出,联系人姓名这一列对应的常量是 ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,联系人手机号这一列对应的常量是 ContactsContract.CommonDataKinds.Phone.NUMBER。两个数据都取出之后,将它们进行拼接,并且中间加上换行符,然后将拼接后的数据添加到 ListView 里。最后千万不要忘记将 Cursor 对象关闭掉。

当然了,读取系统联系人也是需要声明权限的,因此修改 AndroidManifest.xml 中的代码,如下所示:

<uses-permission android:name="android.permission.READ_CONTACTS" />
编译运行看效果:

三、自定义内容提供器

1、创建内容提供器的步骤

前面已经提到过,如果想要实现跨程序共享数据的功能,官方推荐的方式就是使用内容提供器,可以通过新建一个类去继承 ContentProvider 的方式来创建一个自己的内容提供器。ContentProvider 类中有六个抽象方法,我们在使用子类继承它的时候,需要将这六个方法全部重写。

新建 MyProvider 继承自 ContentProvider,代码如下所示:
/**
 * Created by   : WGH.
 */
public class MyProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
        return null;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
        return 0;
    }
}

在这六个方法中,相信大多数你都已经非常熟悉了,我再来简单介绍一下吧。

1. onCreate()

初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回 true 表示内容提供器初始化成功,返回 false 则表示失败。注意,只有当存在ContentResolver 尝试访问我们程序中的数据时,内容提供器才会被初始化。

2. query()

从内容提供器中查询数据。使用 uri 参数来确定查询哪张表, projection 参数用于确定查询哪些列, selection 和 selectionArgs 参数用于约束查询哪些行, sortOrder 参数用于对结果进行排序, 查询的结果存放在 Cursor 对象中返回。

3. insert()

向内容提供器中添加一条数据。使用 uri 参数来确定要添加到的表,待添加的数据保存在 values 参数中。添加完成后,返回一个用于表示这条新记录的 URI。

4. update()

更新内容提供器中已有的数据。使用 uri 参数来确定更新哪一张表中的数据,新数据保存在 values 参数中, selection 和 selectionArgs 参数用于约束更新哪些行, 受影响的行数将作为返回值返回。

5. delete()

从内容提供器中删除数据。使用 uri 参数来确定删除哪一张表中的数据, selection 和 electionArgs 参数用于约束删除哪些行, 被删除的行数将作为返回值返回。

6. getType()

根据传入的内容 URI 来返回相应的 MIME 类型。

可以看到,几乎每一个方法都会带有 Uri 这个参数,这个参数也正是调用 ContentResolver 的增删改查方法时传递过来的。而现在,我们需要对传入的 Uri 参数进行解析,从中分析出调用方期望访问的表和数据。

回顾一下,一个标准的内容 URI 写法是这样的:

content://com.wgh.willflowcontentprovider.provider/table1

这就表示调用方期望访问的是 com.com.wgh.willflowcontentprovider 这个应用的 table1 表中的数据。除此之外,我们还可以在这个内容 URI 的后面加上一个 id,如下所示:

content://com.wgh.willflowcontentprovider.provider/table1/1

这就表示调用方期望访问的是 com.wgh.willflowcontentprovider 这个应用的 table1 表中 id 为 1 的数据。

内容 URI 的格式主要就只有以上两种,以路径结尾就表示期望访问该表中所有的数据,以 id 结尾就表示期望访问该表中拥有相应 id 的数据。我们可以使用通配符的方式来分别匹配这两种格式的内容 URI,规则如下:

  • “*”:表示匹配任意长度的任意字符
  • “#”:表示匹配任意长度的数字

所以,一个能够匹配任意表的内容 URI 格式就可以写成:

content://com.wgh.willflowcontentprovider.provider/*

而一个能够匹配 table1 表中任意一行数据的内容 URI 格式就可以写成:

content://com.wgh.willflowcontentprovider.provider/#

接着,我们再借助 UriMatcher 这个类就可以轻松地实现匹配内容 URI 的功能。UriMatcher 中提供了一个 addURI() 方法,这个方法接收三个参数,可以分别把权限、路径和一个自定义代码传进去。这样,当调用 UriMatcher 的 match() 方法时,就可以将一个 Uri 对象传入,返回值是某个能够匹配这个 Uri 对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了。

修改 MyProvider 中的代码,如下所示:

    public static final int TABLE1_DIR = 0;
    public static final int TABLE1_ITEM = 1;
    public static final int TABLE2_DIR = 2;
    public static final int TABLE2_ITEM = 3;

    private static UriMatcher uriMatcher;

    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI("com.wgh.willflowcontentprovider.provider", "table1", TABLE1_DIR);
        uriMatcher.addURI("com.wgh.willflowcontentprovider.provider ", "table1/#", TABLE1_ITEM);
        uriMatcher.addURI("com.wgh.willflowcontentprovider.provider ", "table2", TABLE2_DIR);
        uriMatcher.addURI("com.wgh.willflowcontentprovider.provider ", "table2/#", TABLE2_ITEM);
    }

    @Override
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
        switch (uriMatcher.match(uri)) {
            case TABLE1_DIR:
                // 查询table1表中的所有数据
                break;
            case TABLE1_ITEM:
                // 查询table1表中的单条数据
                break;
            case TABLE2_DIR:
                // 查询table2表中的所有数据
                break;
            case TABLE2_ITEM:
                // 查询table2表中的单条数据
                break;
            default:
                break;
        }
        return null;
    }

可以看到, MyProvider 中新增了四个整型常量,其中 TABLE1_DIR 表示访问 table1 表中的所有数据,TABLE1_ITEM 表示访问 table1 表中的单条数据, TABLE2_DIR 表示访问 table2 表中的所有数据, TABLE2_ITEM 表示访问 table2 表中的单条数据。接着在静态代码块里我们创建了 UriMatcher 的实例,并调用 addURI() 方法,将期望匹配的内容 URI 格式传递进去,注意这里传入的路径参数是可以使用通配符的。然后当 query() 方法被调用的时候,就会通过 UriMatcher 的 match() 方法对传入的 Uri 对象进行匹配,如果发现 UriMatcher 中某个内容 URI 格式成功匹配了该 Uri 对象,则会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的到底是什么数据了。

上述代码只是以 query() 方法为例做了个示范,其实 insert()、 update()、 delete() 这几个方法的实现也是差不多的,它们都会携带 Uri 这个参数,然后同样利用 UriMatcher 的 match() 方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了。

除此之外,还有一个方法你会比较陌生,即 getType() 方法。它是所有的内容提供器都必须提供的一个方法,用于获取 Uri 对象所对应的 MIME 类型。 一个内容 URI 所对应的 MIME 字符串主要由三部分组分, Android 对这三个部分做了如下格式规定:

  • 必须以 vnd 开头。
  • 如果内容 URI 以路径结尾,则后接 android.cursor.dir/,如果内容 URI 以 id 结尾,则后接 android.cursor.item/。
  • 最后接上 vnd.<authority>.<path>。

所以,对于 content://com.wgh.willflowcontentprovider.provider/table1 这个内容 URI,它所对应的 MIME 类型就可以写成:

vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.table1

对于 content://com.wgh.willflowcontentprovider.provider/table1/1 这个内容 URI,它所对应的 MIME 类型就可以写成:

vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.table1

现在我们可以继续完善 MyProvider 中的内容了,这次来实现 getType()方法中的逻辑,代码如下所示:

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        switch (uriMatcher.match(uri)) {
            case TABLE1_DIR:
                return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.table1";
            case TABLE1_ITEM:
                return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.table1";
            case TABLE2_DIR:
                return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.table2";
            case TABLE2_ITEM:
                return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.table2";
            default:
                break;
        }
        return null;
    }

到这里,一个完整的内容提供器就创建完成了,现在任何一个应用程序都可以使用 ContentResolver 来访问我们程序中的数据。那么前面所提到的,如何才能保证隐私数据不会泄漏出去呢?其实多亏了内容提供器的良好机制,这个问题在不知不觉中已经被解决了。因为所有的 CRUD 操作都一定要匹配到相应的内容 URI 格式才能进行的,而我们当然不可能向 UriMatcher 中添加隐私数据的 URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。那么下面我们就来实战一下,真正体验一回跨程序数据共享的功能。

2、实现跨程序数据共享

简单起见,我们还是在面创建的代码基础上继续开发,通过内容提供器来给它加入外部访问接口。首先将 MyDatabaseHelper 中使用 Toast 弹出创建数据库成功的提示去除掉,因为跨程序访问时我们不能直接使用 Toast。然后添加一个 DatabaseProvider 类,代码如下所示:

/**
 * Created by   : WGH.
 */
public class DatabaseProvider  extends ContentProvider {
    public static final int BOOK_DIR = 0;
    public static final int BOOK_ITEM = 1;
    public static final int CATEGORY_DIR = 2;
    public static final int CATEGORY_ITEM = 3;
    public static final String AUTHORITY = "com.wgh.willflowcontentprovider.provider";

    private static UriMatcher uriMatcher;
    private MyDatabaseHelper dbHelper;

    static {
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
        uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
        uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);
        uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);
    }

    @Override
    public boolean onCreate() {
        dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        // 查询数据
        SQLiteDatabase db = dbHelper.getReadableDatabase();
        Cursor cursor = null;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                cursor = db.query("Book", projection, "id = ?", new String[]{ bookId }, null, null, sortOrder);
                break;
            case CATEGORY_DIR:
                cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                cursor = db.query("Category", projection, "id = ?", new String[]{ categoryId }, null, null, sortOrder);
                break;
            default:
                break;
        }
        return cursor;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.book";
            case BOOK_ITEM:
                return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.book";
            case CATEGORY_DIR:
                return "vnd.android.cursor.dir/vnd.com.wgh.willflowcontentprovider.provider.category";
            case CATEGORY_ITEM:
                return "vnd.android.cursor.item/vnd.com.wgh.willflowcontentprovider.provider.category";
        }
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
        // 添加数据
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Uri uriReturn = null;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
            case BOOK_ITEM:
                long newBookId = db.insert("Book", null, contentValues);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
                break;
            case CATEGORY_DIR:
            case CATEGORY_ITEM:
                long newCategoryId = db.insert("Category", null, contentValues);
                uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId);
                break;
            default:
                break;
        }
        return uriReturn;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        // 删除数据
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int deletedRows = 0;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                deletedRows = db.delete("Book", selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                deletedRows = db.delete("Book", "id = ?", new String[] { bookId });
                break;
            case CATEGORY_DIR:
                deletedRows = db.delete("Category", selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                deletedRows = db.delete("Category", "id = ?", new String[]
                        { categoryId });
                break;
            default:
                break;
        }
        return deletedRows;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String selection, @Nullable String[] selectionArgs) {
        // 更新数据
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        int updatedRows = 0;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                updatedRows = db.update("Book", contentValues, selection, selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId = uri.getPathSegments().get(1);
                updatedRows = db.update("Book", contentValues, "id = ?", new String[]{ bookId });
                break;
            case CATEGORY_DIR:
                updatedRows = db.update("Category", contentValues, selection, selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                updatedRows = db.update("Category", contentValues, "id = ?", new String[]{ categoryId });
                break;
            default:
                break;
        }
        return updatedRows;
    }
}

首先在类的一开始,同样是定义了四个常量,分别用于表示访问 Book 表中的所有数据、访问 Book 表中的单条数据、访问 Category 表中的所有数据和访问 Category 表中的单条数据。然后在静态代码块里对 UriMatcher 进行了初始化操作,将期望匹配的几种 URI 格式添加了进去。接下来就是每个抽象方法的具体实现了,我们分别来看一下。

(1)先来看下 onCreate() 方法

这个方法的代码很短,就是创建了一个 MyDatabaseHelper 的实例,然后返回 true 表示内容提供器初始化成功,这时数据库就已经完成了创建或升级操作。

(2)接着看一下 query() 方法

在这个方法中先获取到了 SQLiteDatabase 的实例,然后根据传入的 Uri 参数判断出用户想要访问哪张表,再调用 SQLiteDatabase 的 query() 进行查询,并将 Cursor 对象返回就好了。注意当访问单条数据的时候有一个细节,这里调用了 Uri 对象的 getPathSegments() 方法,它会将内容 URI 权限之后的部分以“ /” 符号进行分割,并把分割后的结果放入到一个字符串列表中,那这个列表的第 0 个位置存放的就是路径,第 1 个位置存放的就是 id 了。得到了 id 之后,再通过 selection 和 selectionArgs 参数进行约束,就实现了查询单条数据的功能。

(3)再往后就是 insert() 方法

同样它也是先获取到了 SQLiteDatabase 的实例,然后根据传入的 Uri 参数判断出用户想要往哪张表里添加数据,再调用 SQLiteDatabase 的 insert() 方法进行添加就可以了。注意 insert() 方法要求返回一个能够表示这条新增数据的 URI,所以我们还需要调用 Uri.parse() 方法来将一个内容 URI 解析成 Uri 对象,当然这个内容 URI 是以新增数据的 id 结尾的。

(4)接下来就是 update()方法了

这个方法也是先获取 SQLiteDatabase 的实例,然后根据传入的 Uri 参数判断出用户想要更新哪张表里的数据,再调用 SQLiteDatabase 的 update() 方法进行更新就好了,受影响的行数将作为返回值返回。

(5)下面是 delete()方法

这里仍然是先获取到 SQLiteDatabase 的实例,然后根据传入的 Uri 参数判断出用户想要删除哪张表里的数据,再调用 SQLiteDatabase 的 delete() 方法进行删除就好了,被删除的行数将作为返回值返回。

这样我们就将内容提供器中的代码全部编写完了,不过离实现跨程序数据共享的功能还差了一小步,因为还需要将内容提供器在 AndroidManifest.xml 文件中注册才可以,如下所示:

        <provider
            android:name=".DatabaseProvider"
            android:authorities="com.wgh.willflowcontentprovider.provider"
            android:exported="true">
        </provider>

这里我们使用了 <provider> 标签来对 DatabaseProvider 这个内容提供器进行注册,在 android:name 属性中指定了该类的全名,又在 android:authorities 属性中指定了该内容提供器的权限。

现在这个项目就已经拥有了跨程序共享数据的功能了。首先运行一下项目,然后接着关闭掉这个项目,并创建一个新项目WillFlowProviderTest, 我们就将通过WillFlowProviderTest中的代码程序去访问刚刚启动又关闭掉的这个项目中的数据。

先来编写一下布局文件 activity_main.xml :
<android.support.constraint.ConstraintLayout 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="com.wgh.willflowprovidertest.MainActivity">

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="添加"
        android:textColor="#05d602"
        android:textSize="25dp" />

    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="查询"
        android:textColor="#07b7f2"
        android:textSize="25dp" />

    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="修改"
        android:textColor="#f28007"
        android:textSize="25dp" />

    <Button
        android:id="@+id/button4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="删除"
        android:textColor="#f20755"
        android:textSize="25dp" />

</android.support.constraint.ConstraintLayout>

布局文件放置了四个按钮,分别用于添加、查询、修改和删除数据的。

然后修改 MainActivity 中的代码,如下所示:
    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.button1:
                // 添加数据
                Uri uriAdd = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book");
                ContentValues values = new ContentValues();
                values.put("name", "WillFlow");
                values.put("author", "WGH");
                values.put("pages", 525);
                values.put("price", 16.18);
                Uri newUri = getContentResolver().insert(uriAdd, values);
                newId = newUri.getPathSegments().get(1);
                break;
            case R.id.button2:
                // 查询数据
                Uri uriQuery = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book");
                Cursor cursor = getContentResolver().query(uriQuery, null, null, null, null);
                if (cursor != null) {
                    while (cursor.moveToNext()) {
                        String name = cursor.getString(cursor.getColumnIndex("name"));
                        String author = cursor.getString(cursor.getColumnIndex("author"));
                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        double price = cursor.getDouble(cursor.getColumnIndex("price"));
                        Log.d(TAG, "book name : " + name + ", author : " + author + ", pages : " + pages + ", price : " + price);
                        Toast.makeText(MainActivity.this, "book name : " + name + ", author : " + author + ", pages : " + pages + ", price : " + price, Toast.LENGTH_SHORT).show();
                    }
                    cursor.close();
                }
                break;
            case R.id.button3:
                // 更新数据
                Uri uriUpdate = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book/" + newId);
                ContentValues contentValues = new ContentValues();
                contentValues.put("name", "A Flow of Wills");
                contentValues.put("pages", 1314);
                contentValues.put("price", 29.26);
                getContentResolver().update(uriUpdate, contentValues, null, null);
                break;
            case R.id.button4:
                // 删除数据
                Uri uriDelete = Uri.parse("content://com.wgh.willflowcontentprovider.provider/book/" + newId);
                getContentResolver().delete(uriDelete, null, null);
                break;
        }
    }

我们分别在这四个按钮的点击事件里面处理了增删改查的逻辑。

添加数据的时候,首先调用了 Uri.parse() 方法将一个内容 URI 解析成 Uri 对象,然后把要添加的数据都存放到 ContentValues 对象中,接着调用 ContentResolver 的 insert() 方法执行添加操作就可以了。注意 insert() 方法会返回一个 Uri 对象,这个对象中包含了新增数据的 id,我们通过 getPathSegments() 方法将这个 id 取出,稍后会用到它。

查询数据的时候,同样是调用了 Uri.parse() 方法将一个内容 URI 解析成 Uri 对象,然后调用 ContentResolver 的 query() 方法去查询数据,查询的结果当然还是存放在 Cursor 对象中的。之后对 Cursor 进行遍历,从中取出查询结果,并一一打印出来。

更新数据的时候,也是先将内容 URI 解析成 Uri 对象,然后把想要更新的数据存放到 ContentValues 对象中,再调用 ContentResolver 的 update() 方法执行更新操作就可以了。注意这里我们为了不想让 Book 表中其他的行受到影响,在调用 Uri.parse() 方法时,给内容 URI 的尾部增加了一个 id,而这个 id 正是添加数据时所返回的。这就表示我们只希望更新刚刚添加的那条数据, Book 表中的其他行都不会受影响。

删除数据的时候,也是使用同样的方法解析了一个以 id 结尾的内容 URI,然后调用 ContentResolver 的 delete() 方法执行删除操作就可以了。由于我们在内容 URI 里指定了一个id,因此只会删掉拥有相应 id 的那行数据, Book 表中的其他数据都不会受影响。

编译运行看效果:

由此可以看出,我们的跨程序共享数据功能已经成功实现了!现在不仅是 WillFlowProviderTest 程序,任何一个程序都可以轻松访问 WillFlowContentProvider 中的数据,而且我们还丝毫不用担心隐私数据泄漏的问题。

点此进入:GitHub开源项目“爱阅”

感谢优秀的你跋山涉水看到了这里,欢迎关注下让我们永远在一起!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容