ContentProvider 相关

一. 介绍

ContentProvider 管理对结构化数据集的访问。它们封装数据,并提供用于定义数据安全性的机制。 ContentProvider 是连接一个进程中的数据与另一个进程中运行的代码的标准界面。ContentProvider 以一个或多个表(与

在关系型数据库中找到的表类似)的形式将数据呈现给外部应用。 行表示提供程序收集的某种数据类型的实例,行中的每个列表示为实例收集的每条数据。我们对于ContentProvider 的主要使用就是:

(1)访问系统数据,如联系人、多媒体等

(2)存储数据

(3)提供给其他应用访问本应用数据的接口

二. 使用

应用从具有 ContentResolver 客户端对象的内容提供程序访问数据。 此对象具有调用提供程序对象(ContentProvider 的某个具体子类的实例)中同名方法的方法。 ContentResolver 方法可提供持续存储的基本“CRUD”(创建、检索、更新和删除)功能。

客户端应用进程中的 ContentResolver 对象和拥有提供程序的应用中的 ContentProvider 对象可自动处理跨进程通信。 ContentProvider 还可充当其数据存储区和表格形式的数据外部显示之间的抽象层。

注意:访问ContentProvider 时可能需要一些权限,这在后面会提到。

1. 创建ContentProvider

在着手开发ContentProvider 之前要考虑一下是否真的有必要这样做,如果我们想提供下列一项或多项功能,则需要进行开发:

  • 为其他应用提供复杂的数据或文件

  • 允许用户将复杂的数据从我们的应用复制到其他应用中

  • 使用搜索框架提供自定义搜索建议

如果完全是在自己的应用中使用,则不需要ContentProvider,使用 SQLite 数据库即可。

开发ContentProvider 有以下步骤:

  • 为我们的数据设计原始存储,内容提供程序以两种方式提供数据:
    文件数据 —— 通常存储在文件中的数据,如照片、音频或视频。 将文件存储在应用的私有空间内。 ContentProvider 可以应其他应用发出的文件请求提供文件句柄。
    “结构化”数据 —— 通常存储在数据库、数组或类似结构中的数据。 以兼容行列表的形式存储数据。行表示实体,如人员或库存项目。 列表示实体的某项数据,如人员的姓名或商品的价格。 此类数据通常存储在 SQLite 数据库中,但可以使用任何类型的持久存储。

  • 定义 ContentProvider 类及其所需方法的具体实现。 这个类是数据与 Android 系统其余部分之间的接口。

  • 定义提供程序的授权字符串、其内容 URI 以及列名称。如果想让ContentProvider 的应用处理 Intent,则还要定义 Intent 操作、Extra 数据以及标志。 此外,还要定义想要访问我们的数据的应用必须具备的权限。 可以考虑在一个单独的协定类中将所有这些值定义为常量;以后可以将此类公开给其他开发者。

  • 添加其他可选部分,如示例数据或可以在提供程序与云数据之间同步数据的 AbstractThreadedSyncAdapter 实现。

(1)数据设计

数据存储的方式上面也有介绍,但是也有一些设计的技巧:

  • 表数据应始终具有一个“主键”列,提供程序将其作为与每行对应的唯一数字值加以维护。 可以使用此值将该行链接到其他表中的相关行(将其用作“外键”)。 且最好使用BaseColumns._ID 命名。

  • 如果提供位图图像或其他非常庞大的文件导向型数据,要将数据存储在一个文件中,然后间接提供这些数据,而不是直接将其存储在表中。 如果执行了此操作,则需要告知提供程序的用户,他们需要使用 ContentResolver 文件方法来访问数据;

  • 使用二进制大型对象 (BLOB) 数据类型存储大小或结构会发生变化的数据。 例如,可以使用 BLOB 列来存储协议缓冲区或 JSON 结构。

  • 也可以使用 BLOB 来实现独立于架构的表。在这类表中,需要以 BLOB 形式定义一个主键列、一个 MIME 类型列以及一个或多个通用列。 这些 BLOB 列中数据的含义通过 MIME 类型列中的值指示。 这样一来,就可以在同一个表中存储不同类型的行。 举例来说,联系人提供程序的“数据”表 ContactsContract.Data 便是一个独立于架构的表。

(2)设计URI

内容 URI 是用于在提供程序中标识数据的 URI。内容 URI 包括整个ContentProvider 的符号名称(其authorities)和一个指向表的名称(path)。 当调用客户端方法来访问提供程序中的表时,该表的内容 URI 将是其参数之一。

比如用户字典的URI

content://user_dictionary/words

其中,user_dictionary 字符串是提供程序的authority ,words 字符串是表的路径。 字符串 content://(schema)始终显示,并将此标识为内容 URI。

ContentProvider 通常具有单一authority,该authority 充当其 Android 内部名称,通常可以采用 "包名+ .provider"的形式,如 "com.whx.test.provider" ;

通常通过追加指向单个表的路径来根据authority 创建内容 URI。 例如,有两个表:table1 和 table2,则可以通过合并authority 来生成 内容 URI com.whx.test.provider/table1 和 com.whx.test.provider/table2。path 并不限定于单个段,也无需为每一级path都创建一个表。

处理URI 中的ID

按照惯例,ContentProvider 通过接受末尾具有行所对应 ID 值的内容 URI 来提供对表中单个行的访问。 同样按照惯例,提供程序会将该 ID 值与表的 _ID 列进行匹配,并对匹配的行执行请求的访问。

这一惯例为访问提供程序的应用的常见设计模式提供了便利。应用会对提供程序执行查询,并使用 CursorAdapter 以 ListView 显示生成的 Cursor。 定义 CursorAdapter 的条件是, Cursor 中的其中一个列必须是 _ID

用户随后从 UI 上显示的行中选取其中一行,以查看或修改数据。 应用会从支持 ListView 的 Cursor 中获取对应行,获取该行的 _ID 值,将其追加到内容 URI,然后向提供程序发送访问请求。 然后,提供程序便可对用户选取的特定行执行查询或修改。

(3)继承ContentProvider 类,并实现相关的方法

其他应用在访问数据时都是通过ContentResolver 来实现,ContentResolver最终都调用了ContentProvider,ContentProvider是Android的四大组件之一,是一个抽象类,我们要对其进行继承并实现相关方法来实现对数据的操作。

有以下方法是必须要实现的:onCreate、insert、query、update、delete、getType。

这些CRUD方法和ContentResolver 中的方法签名是相同的。

在实现这些方法时应考虑以下事项:

  • 所有这些方法(onCreate() 除外)都可由多个线程同时调用,因此它们必须是线程安全方法。

  • 避免在 onCreate() 中执行长时间操作。将初始化任务推迟到实际需要时进行。

  • 尽管必须实现这些方法,但只需返回要求的数据类型,无需执行任何其他操作。 例如,有时可能想防止其他应用向某些表插入数据。 要实现此目的,可以忽略 insert() 调用并返回 0。

实现 onCreate() 方法

Android 系统会在启动ContentProvider 时调用 onCreate()。只应该在此方法中执行运行快速的初始化任务,并将数据库创建和数据加载推迟到提供程序实际收到数据请求时进行。 如果在 onCreate() 中执行长时间的任务,则会减慢提供程序的启动速度, 进而减慢提供程序对其他应用的响应速度。

实现 query() 方法

ContentProvider.query() 方法必须返回 Cursor 对象。如果失败,则会引发 Exception。 如果使用 SQLite 数据库作为数据存储,则只需返回由 SQLiteDatabase 类的其中一个 query() 方法返回的 Cursor。 如果查询不匹配任何行,应该返回一个 Cursor 实例(其 getCount() 方法返回 0)。只有当查询过程中出现内部错误时,才应该返回 null。

如果不使用 SQLite 数据库作为数据存储,就应该使用 Cursor 的其中一个具体子类。 例如,在 MatrixCursor 类实现的游标中,每一行都是一个 Object 数组。 对于此类,请使用 addRow() 来添加新行。

注意,Android 系统必须能够跨进程边界传播 Exception,这些异常可能有助于处理查询错误。

实现 insert() 方法

insert() 方法会使用 ContentValues 参数中的值向相应表中添加新行。 如果 ContentValues 参数中未包含列名称,可能需要在ContentProvider 代码或数据库架构中提供其默认值。

此方法应该返回新行的内容 URI。要想构建此方法,请使用 withAppendedId() 向表的内容 URI 追加新行的 _ID(或其他主键)值。

实现 delete() 方法

delete() 方法不需要从数据存储中实际删除行。 如果将同步适配器与提供程序一起使用,应该考虑为已删除的行添加“删除”标志,而不是将行整个移除。 同步适配器可以检查是否存在已删除的行,并将它们从服务器中移除,然后再将它们从提供程序中删除。

实现 update() 方法

update() 方法采用 insert() 所使用的相同 ContentValues 参数,以及 delete() 和 ContentProvider.query() 所使用的相同 selection 和 selectionArgs 参数。 这样一来,就可以在这些方法之间重复使用代码。

实现 getType() 方法

ContentProvider 类具有两个返回 MIME 类型的方法:getType 和 getStreamTypes(系统在ContentProvider 提供文件时要求实现的方法)

getType() 方法会返回一个 MIME 格式的 String,后者描述内容 URI 参数返回的数据类型。URI 参数可以是模式,而不是特定 URI;在这种情况下,应该返回与匹配该模式的内容 URI 关联的数据类型。对于文本、HTML 或 JPEG 等常见数据类型,getType() 应该为该数据返回标准 MIME 类型。 IANA MIME Media Types 网站上提供了这些标准类型的完整列表。

1)表的MIME 类型

对于指向一个或多个表数据行的内容 URI,getType() 应该以 Android 供应商特有 MIME 格式返回 MIME 类型:

  • 类型部分:vnd

  • 子类型部分:

    • 如果 URI 模式用于单个行:android.cursor.item/

    • 如果 URI 模式用于多个行:android.cursor.dir/

  • ContentProvider 特有部分:vnd..

    提供 值应具有全局唯一性, 值应在对应的 URI 模式中具有唯一性。 适合选择公司的名称或应用 Android 软件包名称的某个部分作为 。 适合选择 URI 关联表的标识字符串作为

例如,如果提供程序的authority是 com.example.app.provider,并且它公开了一个名为 table1 的表,则 table1 中多个行的 MIME 类型是:

vnd.android.cursor.dir/vnd.com.example.provider.table1

对于 table1 的单个行,MIME 类型是:

vnd.android.cursor.item/vnd.com.example.provider.table1

2)文件的MIME 类型

如果ContentProvider 提供文件,就需要实现 getStreamTypes()。 该方法会让ContentProvider 可以为给定内容 URI 返回的文件返回一个 MIME 类型 String 数组。 应该通过 MIME 类型过滤器参数过滤提供的 MIME 类型,以便只返回客户端想处理的那些 MIME 类型。

例如,假设ContentProvider 以 .jpg、.png 和 .gif 格式文件形式提供照片图像。 如果应用调用 ContentResolver.getStreamTypes() 时使用了过滤器字符串 image/*(任何是“图像”的内容),则 ContentProvider.getStreamTypes() 方法应返回数组:

{ "image/jpeg", "image/png", "image/gif"}

如果应用只对 .jpg 文件感兴趣,则可以在调用 ContentResolver.getStreamTypes() 时使用过滤器字符串 */jpeg,ContentProvider.getStreamTypes() 应返回:

{"image/jpeg"}

如果ContentProvider 未提供过滤器字符串中请求的任何 MIME 类型,则 getStreamTypes() 应返回 null。

(4)处理ContentProvider的相关权限

Android 中提供了各方面的权限和访问,关于数据存储方面的权限要点如下

  • 默认情况下,存储在设备内部存储上的数据文件是应用和ContentProvider 的私有数据文件;

  • 应用创建的 SQLiteDatabase 数据库是应用和ContentProvider 的私有数据库;

  • 默认情况下,保存到外部存储的数据文件是公用并可全局读取的数据文件。 无法使用ContentProvider 来限制对外部存储内文件的访问,因为其他应用可以使用其他 API 调用来对它们执行读取和写入操作;

  • 用于在设备的内部存储上打开或创建文件或 SQLite 数据库的方法调用可能会为所有其他应用同时授予读取和写入访问权限。 如果将内部文件或数据库用作提供程序的存储区,并向其授予“可全局读取”或“可全局写入”访问权限,那么在清单文件中为ContentProvider 设置的权限不会保护我们的数据。 内部存储中文件和数据库的默认访问权限是“私有”,对于ContentProvider 的存储区,不应该更改此权限。

如果想使用ContentProvider 权限来控制对数据的访问,则应将数据存储在内部文件、SQLite 数据库或“云”中(例如,远程服务器上),而且我们应该保持文件和数据库被应用所私有。

即使底层数据为私有数据,所有应用仍可从ContentProvider 读取数据或向其写入数据,因为在默认情况下,ContentProvider 未设置权限。 要想改变这种情况,可以使用属性或 元素的子元素在manifest 文件中为ContentProvider 设置权限。 可以设置适用于整个提供程序、特定表甚至特定记录的权限,或者设置同时适用于这三者的权限。

可以通过manifest 文件中的一个或多个 元素为ContentProvider 定义权限。要使权限对我们的ContentProvider 具有唯一性,需要为 android:name 属性使用 Java 风格作用域。 例如,将读取权限命名为 com.whx.test.provider.permission.READ_PROVIDER。

ContentProvider 提供的权限有不同的作用域,更细化的权限优先于作用域更大的权限,下面是一些权限的作用域:

  • 统一读写提供程序级别权限
    一个同时控制对整个提供程序读取和写入访问的权限,通过 元素的 android:permission 属性指定。

  • 单独的读取和写入提供程序级别权限
    针对整个提供程序的读取权限和写入权限。您可以通过 元素的 android:readPermission 属性和 android:writePermission 属性 指定它们。它们优先于 android:permission 所需的权限。

  • 路径级别权限针对提供程序中内容 URI 的读取、写入或读取/写入权限。
    可以通过 元素的 子元素指定想控制的每个 URI。 对于指定的每个Content URI,都可以指定读取/写入权限、读取权限或写入权限,或同时指定所有三种权限。 读取权限和写入权限优先于读取/写入权限。 此外,路径级别权限优先于提供程序级别权限。

  • 临时权限
    一种权限级别,即使应用不具备通常需要的权限,该级别也能授予对应用的临时访问权限。 临时访问功能可减少应用需要在其清单文件中请求的权限数量。 当启用临时权限时,只有持续访问所有数据的应用才需要“永久性”提供程序访问权限。
    假设需要实现电子邮件提供程序和应用的权限,如果想允许外部图像查看器应用显示我们ContentProvider 中的照片附件, 为了在不请求权限的情况下为图像查看器提供必要的访问权限,可以为照片的内容 URI 设置临时权限。 对我们的电子邮件应用进行相应设计,使应用能够在用户想要显示照片时向图像查看器发送一个 Intent,其中包含照片的内容 URI 以及权限标志。 图像查看器可随后查询我们的电子邮件提供程序以检索照片,即使查看器不具备对ContentProvider 的正常读取权限,也不受影响。
    要想启用临时权限,请设置 元素的 android:grantUriPermissions 属性,或者向 元素添加一个或多个 子元素。如果使用了临时权限,则每当我们从ContentProvider 中移除对某个Content URI 的支持,并且该Content URI 关联了临时权限时,都需要调用 Context.revokeUriPermission()。
    该属性的值决定可访问的提供程序范围。 如果该属性设置为 true,则系统会向整个提供程序授予临时权限,该权限将替代我们的ContentProvider 级别或路径级别权限所需的任何其他权限。
    如果此标志设置为 false,则必须向 元素添加 子元素。每个子元素都指定授予的临时权限所对应的一个或多个内容 URI。
    要向应用授予临时访问权限,Intent 必须包含 FLAG_GRANT_READ_URI_PERMISSION 和/或 FLAG_GRANT_WRITE_URI_PERMISSION 标志。 它们通过 setFlags() 方法进行设置。
    如果 android:grantUriPermissions 属性不存在,则假设其为 false。

(5)manifest 文件中注册

ContentProvider 是Android 的四大组件之一,所以要想使用,必须在manifest 文件中进行注册,像上面提到的设置权限等,也是在注册时设置。

元素下,必须设置 android:authorities 属性和android:name属性。另外还有一些其他属性,如权限信息,以及下面的启动和控制属性,信息属性等。

启动和控制属性:

这些属性决定 Android 系统如何以及何时启动提供程序、提供程序的进程特性以及其他运行时设置:

  • android:enabled:允许系统启动提供程序的标志。

  • android:exported:允许其他应用使用此提供程序的标志。

  • android:initOrder:此提供程序相对于同一进程中其他提供程序的启动顺序。

  • android:multiProcess:允许系统在与调用客户端相同的进程中启动提供程序的标志。

  • android:process:应在其中运行提供程序的进程的名称。

  • android:syncable:指示提供程序的数据将与服务器上的数据同步的标志。

信息属性:

提供程序的可选图标和标签:

  • android:icon:包含提供程序图标的可绘制对象资源。 该图标出现在Settings > Apps > All 中应用列表内的提供程序标签旁;

  • android:label:描述提供程序和/或其数据的信息标签。 该标签出现在Settings > Apps > All中的应用列表内。

三. ContentProvider 的创建实例

MyContentProvider

 public class MyContentProvider extends ContentProvider{

    private static final int TEST = 100;

    private static final String DB_NAME = "my_database";
    private static final String TABLE_NAME = "main";
    private static final String SQL_CREATE = "CREATE TABLE " + TABLE_NAME +
            " (" +
            "_id INTEGER PRIMARY KEY, " +
            "word TEXT, " +
            "frequency INTEGER, " +
            "locale TEXT )";

    private MyDatabaseHelper mDbHelper;
    private SQLiteDatabase db;

    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        sUriMatcher.addURI(MyContract.CONTENT_AUTHORITY, MyContract.PATH_TEST, TEST);
    }
    @Override
    public boolean onCreate() {
        mDbHelper = new MyDatabaseHelper(getContext());
        db = mDbHelper.getWritableDatabase();

        return true;
    }

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

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        Uri returnUri = Uri.EMPTY;
        long _id;

        switch (sUriMatcher.match(uri)) {
            case TEST:
                _id = db.insert(MyContract.PATH_TEST, null, values);
                if (_id > 0) {
                    returnUri = MyContract.buildUri(_id);
                } else {
                    throw new SQLException("failed to insert");
                }
                break;
        }
        return returnUri;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {

        switch (sUriMatcher.match(uri)) {
            case TEST:
                db.update(MyContract.PATH_TEST, values, selection, selectionArgs);
        }
        return 0;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        Cursor cursor = null;

        switch (sUriMatcher.match(uri)) {
            case TEST:
                cursor = db.query(MyContract.PATH_TEST, projection, selection, selectionArgs, null, null, sortOrder);
                break;
        }
        return cursor;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        int res = 0;
        switch (sUriMatcher.match(uri)) {
            case TEST:
                res = db.delete(TABLE_NAME, selection, selectionArgs);
        }
        return res;
    }

    static class MyDatabaseHelper extends SQLiteOpenHelper {

        public MyDatabaseHelper(Context context) {
            super(context, DB_NAME, null, 1);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(SQL_CREATE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

        }
    }
}

你可能感兴趣的:(ContentProvider 相关)