Creating a Content Provider

content provider管理了对数据中央仓库存储的访问。我们可以说通过在清单文件中的配置和在app中的一个或多个类来实现它。其中一个实现类需要继承ContentProvider,它是provider和其它app的接口。虽然content provider是用来服务于其它app的,我们的app中activity也可以让用户来查询和修改provider的数据。

本文会讨论创建cotent provider和步骤和常用的API.

创建之前

在创建之前,需要做以下几点:
1.确定是否需要content provider. 我们需要创建content provider时,通常有以下几个特点:
a.向其它app提供复杂数据和文档。
b.允许用户从app中复制复杂的数据到其它app.
c.使用搜索框架时提供自定义的搜索建议。
如果只是在自己的app内部使用,那么不需要提供一个provider就可以使用SQLite数据库。
2.如果还没有完成这个操作,请参阅 Content Provider Basics。

接下来,按照以下的步骤来创建provider:

  1. 设计数据的存储方式。content provider有两种方式来提供数据:
    a.文件。 数据存储在文件中,比如相片,音频或者视频。在app的私有目录下存储这些文件。当其它app请求一个文件时,provider可以提供文件的句柄。
    b.结构化数据。 数据存储在数据库中,数组或者其它类似的结构。和表的行列结构类似的结构存储。一行代表了一个对象,比如一个人或者库存中的一个货物。一列代表了对象的属性,比如,人的名字或者货物的价格。通常存储这样的数据是使用数据库,但我们也可以使用其它持久化的存储。参阅Designing Data Storage.

  2. 定义ContentProvider的实现类和它的方法。这个类是系统和数据的接口。参阅Implementing the ContentProvider Class。

  3. 定义provider的授权string,content URI和列名称。如果想要provider的app还响应intent,也需要定义intent action, extras data和flgas. 定义其它app来访问我们的数据所需要的权限。可以在一个独立的contract class中定义这些常量。然后把这个类暴露给其它开发者。参阅Designing Content URIs和Intents and Data Access.
    4.添加其它可选的部分,比如,示例数据和可以对provider和云端数据进行同步的AbstractThreadedSyncAdapter实现.

设计数据存储

一个content provider是用来格式化存储数据的接口。在创建结果之前,我们必须确定?如何存储数据。我们可以以喜欢的方式去存储,然后设计必要的读写接口。

安卓系统有一些数据存储的技术:
1.系统有SQLite数据库的API,系统自己的providers使用这些API来进行表格式的数据存储。SQLiteOpenHelper可以帮助我们创建数据库,SQLiteDatabase是访问数据库的基类。

不是必须要使用数据库来实现存储。provider对外表现像一组关系型数据库的表,但是不是说他的内部实现也必须如此。

2.存储文件数据,系统有一组面向文件的API. 参阅 Data Storage了解更多。如果我们设计一个提供多媒体数据(音乐或者视频)的provider,我们可以使用表和文件结合的方式。

3.网络数据,使用java.net和android.net下的类。我们可以对网络数据和本地数据(比如数据库)进行同步,然后以表的方式或者文件的方式来提供数据。示例Sample Sync Adapter说明了这类同步的操作。

数据设计注意事项

设计provider的数据结构时,应该注意的事项:

表数据应该有一个主键,provider会给每一行提供一个唯一的数字作为主键。 我们可以使用这个值来关联到其它表的主键(作为外键去关联其它)。虽然我们可以使用任意的名称,但是使用BaseColumns._ID是最好的,因为把结果关联到ListView时,需要获取的列中有一个是_ID.

如果我们想要提供图片或者其它文件数据,把数据存储到一个文件中,然后间接存储,而不是直接存储到表中。如果我们这么处理了,需要告知用户,他们需要使用ContentResolver的文件相关方法能才访问数据。

使用二进制大型对象(BLOB)去存储大小或者结构不同的数据。比如,可以使用BLOB这一列来存储protocol buffer或JSON结构。

也可以使用BLOB来实现一个独立schema的表。在这种表中,可以定义一个主键列,一个MIME列,一个或多个BLOB的通用列。BLOB列中的数据可以通过MIME列的值来说明。这就可以让我们在一个表中存储多种类型的数据。Contacts Provider的数据表ContactsContract.Data就是一个这样的例子。

设计 Content URIs
Content URI是一个标识provider中数据的URI. Content URIs包含了整个provider的符号名(授权)和指定固定表或者文件的名称(路径)。还有一个可选的id指向了表中的一行。ContentProvider的每个数据访问方法都需要一个content URI的参数,这样就可以定位表,表中的行或者文件去访问。

参阅Content Provider Basics来了解更多。

设计授权

一个provider通常有单一的authority,它是在系统内部的名称。为了避免和其它provider冲突,我们应该使用网络域名的倒序来作为authority. 因为这也是推荐的包名命名方式,我们可以把provider的authority定义成包含provider的app的包名扩展。比如,如果包名是com.example.,可以使用com.example..provider.

设计一个路径

开发者通常是通过在authority后边追加指向表路径的方式来创建content URI。比如,如果我们有两张表,table1和table2,可以把前边的例子中的authority组合成 com.example..provider/table1和com.example..provider/table2.路径不限定必须是一级,也不需要每张表都有一个路径。

处理content URI的id

通常,provider提供了访问表中的一行的方法,具体是通过接收一个带有id的content URI,这个id是指向行的并且在URI的结尾。provider通常会用这个id去匹配表中的_ID这一列,然后对匹配到的那一行执行查询请求。

这种惯用的方式对设计一个provider是非常有用的。app对provider进行一次查询然后使用CursorAdapter在ListView中展示结果。CursorAdapter的定义需要其中有一列必须是_ID。

用户然后选中一行来查询或者修改数据。app从ListView对应的Cursor中取到那一行,然后取到_ID,把它追加到content URI,再发送请求到provider.provider然后查询或者修改用户选择的那一行数据。

content URI模式

为了帮助我们选择对一个content URI具体的action,provider的API有方便的类UriMatcher,它把content URI的模式映射成为int值。我们可以使用一个switch语句来对content URI或者一种类型的URIS来进行特定的操作。

pattern URI 模式使用通配符匹配content URI:
*: 匹配由任意长度的任何有效字符组成的字符串
#: 匹配由任意长度的数字字符组成的字符串
前边的示例,如果一个provider的authority是com.example.app.provider会识别以下指向表的content URIs:

content://com.example.app.provider/table1: A table called table1.
content://com.example.app.provider/table2/dataset1: A table called dataset1.
content://com.example.app.provider/table2/dataset2: A table called dataset2.
content://com.example.app.provider/table3: A table called table3.

Provider也会识别那些带有id的content URI,比如:content://com.example.app.provider/table3/1 .

下边的content URI模型也是可以的:
content://com.example.app.provider/*
匹配provider的任何content URI。
content://com.example.app.provider/table2/*:
匹配table2的dataset1和dataset2的content URI,但是不匹配table1或者table3.
content://com.example.app.provider/table3/#:
匹配table3中一行的content URI,比如content://com.example.app.provider/table3/6是6这一行。

下边代码是示例UriMatcher的方法是如何工作的。处理表的URI和处理一行的URI采用的方法不同,使用content:///来匹配表,content:////来匹配单行。

addURI方法把一个authority和路径映射成一个int值。match方法返回一个URI的int值。一个switch语句来判断是查询整张表还是某一行记录:

public class ExampleProvider extends ContentProvider {
...
    // Creates a UriMatcher object.
    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        /*
         * The calls to addURI() go here, for all of the content URI patterns that the provider
         * should recognize. For this snippet, only the calls for table 3 are shown.
         */

        /*
         * Sets the integer value for multiple rows in table 3 to 1. Notice that no wildcard is used
         * in the path
         */
        sUriMatcher.addURI("com.example.app.provider", "table3", 1);

        /*
         * Sets the code for a single row to 2. In this case, the "#" wildcard is
         * used. "content://com.example.app.provider/table3/3" matches, but
         * "content://com.example.app.provider/table3 doesn't.
         */
        sUriMatcher.addURI("com.example.app.provider", "table3/#", 2);
    }
...
    // Implements ContentProvider.query()
    public Cursor query(
        Uri uri,
        String[] projection,
        String selection,
        String[] selectionArgs,
        String sortOrder) {
...
        /*
         * Choose the table to query and a sort order based on the code returned for the incoming
         * URI. Here, too, only the statements for table 3 are shown.
         */
        switch (sUriMatcher.match(uri)) {


            // If the incoming URI was for all of table3
            case 1:

                if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
                break;

            // If the incoming URI was for a single row
            case 2:

                /*
                 * Because this URI was for a single row, the _ID value part is
                 * present. Get the last path segment from the URI; this is the _ID value.
                 * Then, append the value to the WHERE clause for the query
                 */
                selection = selection + "_ID = " uri.getLastPathSegment();
                break;

            default:
            ...
                // If the URI is not recognized, you should do some error handling here.
        }
        // call the code to actually do the query
    }

另一个类ContentUris,提供便利的方法来处理带有id的content URI.类Uri和Uri.Builder有便利的方法来解析URI和构建新的URI.

实现ContentProvider

ContentProvider通过处理其它app的请求来管理对一组结构化数据的访问。所有形式的访问都会调用ContentResolver,然后调用ContentProvider的具体方法来访问。

必须的方法

ContentProvider抽象类定义了6个抽象方法来让我们实现。除了onCreate以外所有的方法都是通过一个client的app在尝试访问我们的content provider时调用的。

query()
从provider中获取数据。使用参数来查询,按照固定排序返回对应的行和列。返回的数据是一个Cursor对象。

insert()
向provider中插入新的一行。使用参数来选择目标表和列的值。返回新添加行的content URI.

update()
更新provider中已经有的行。使用参数来选择表和需要更新的行,获取要更新的数据。返回更新的行数。

delete()
从provider中删除行。使用参数来选择表和需要删除的行。返回删除的行数。

getType()
返回content URI对应的MIME类型。参阅Implementing Content Provider MIME Types.

onCreate()
初始化provider.系统在创建provider之后会立即回调这个方法。需要注意的是,只有在一个ContentResolver来访问的时候,prvoider才会真正创建。

注意:这些方法在ContentResolver中都有对应的方法。

我们在实现这些方法时要注意以下几点:

1.除了onCreate之外的所有方法都可以同时多线程调用,所以必须是线程安全的。参阅Processes and Threads来了解多线程。

2.为了避免在onCreate中进行耗时操作,所有初始化的操作尽量在需要的时候再进行。参阅Implementing the onCreate() method。

  1. 虽然我们必须实现这些方法,但是除了返回需要的数据之外,我们的代码可以什么也不做。比如,我们可能想防止其它app插入数据,那么只需要在insert中直接返回0,什么也不处理就可以了。

实现query方法

ContentProvider.query()必须返回一个Cursor,或者失败时抛也异常。如果使用SQLite数据库,我们可以简单地通过SQLite的query方法返回一个Cursor。如果查询没有匹配的行,我们应该返回一个getCount为0的cursor.只有内部错误的时候才应该返回null.

如果没有使用SQLite,就使用一个Cursor的子类。比如,MatrixCursor,它的每一行都是对象数组,使用addRow可以添加新的一行。

请牢记, 安卓系统可以在进程间传递Exception.系统可以为以下异常进行这个操作,可能对处理查询异常非常有帮助:
1.IllegalArgumentException (可以在接收到一个无效的content URI时抛出这个异常)
2.NullPointerException

实现insert方法

使用insert方法可以使用ContentValues参数向一张表中添加新的一行。如果ContentValues参数中没有列名称,我们可能需要在代码中或者在数据库的schema提供一个默认值。

方法的返回值是新添加行的 content URI.在构建这个URI时,需要使用withAppendedId在表的 content URI后边追加 _ID(或者其它的主键),

实现delete方法

delete方法不是必须得进行物理删除。如果使用的是同步adapter,我们可以考虑给删除的行添加一个标记,而不是进行物理删除。同步adapter可以进行检查,从服务端进行删除即可。

实现update方法

update方法接收和insert方法一样的ContentValues参数,和delete方法,query方法一样的查询条件。这可以让我们在不同的方法间重用代码。

实现onCreate方法

系统会在启动provider时调用onCreate。我们应该只进行一些非耗时的初始化操作,推迟那些数据库创建,加载数据等操作,直接接收到数据请求时再进行这些操作。如果在onCreate中进行耗时操作,我们会减慢provider的启动。又导致了provider对其它app的响应变慢。

比如,我们在使用一个SQLite,我们可以在ContentProvider.onCreate()中创建一个SQLiteOpenHelper对象,在第一次打开数据库时再去创建SQL表。为了简化这一过程,第一次调用getWritableDatabase,会自动调用SQLiteOpenHelper.onCreate()。

下边的代码展示了ContentProvider.onCreate() 和SQLiteOpenHelper.onCreate()的交互。第一段代码实现了ContentProvider.onCreate():

public class ExampleProvider extends ContentProvider

    /*
     * Defines a handle to the database helper object. The MainDatabaseHelper class is defined
     * in a following snippet.
     */
    private MainDatabaseHelper mOpenHelper;

    // Defines the database name
    private static final String DBNAME = "mydb";

    // Holds the database object
    private SQLiteDatabase db;

    public boolean onCreate() {

        /*
         * Creates a new helper object. This method always returns quickly.
         * Notice that the database itself isn't created or opened
         * until SQLiteOpenHelper.getWritableDatabase is called
         */
        mOpenHelper = new MainDatabaseHelper(
            getContext(),        // the application context
            DBNAME,              // the name of the database)
            null,                // uses the default SQLite cursor
            1                    // the version number
        );

        return true;
    }

    ...

    // Implements the provider's insert method
    public Cursor insert(Uri uri, ContentValues values) {
        // Insert code here to determine which table to open, handle error-checking, and so forth

        ...

        /*
         * Gets a writeable database. This will trigger its creation if it doesn't already exist.
         *
         */
        db = mOpenHelper.getWritableDatabase();
    }
}

下边的代码实现了SQLiteOpenHelper.onCreate():

...
// A string that defines the SQL statement for creating a table
private static final String SQL_CREATE_MAIN = "CREATE TABLE " +
    "main " +                       // Table's name
    "(" +                           // The columns in the table
    " _ID INTEGER PRIMARY KEY, " +
    " WORD TEXT"
    " FREQUENCY INTEGER " +
    " LOCALE TEXT )";
...
/**
 * Helper class that actually creates and manages the provider's underlying data repository.
 */
protected static final class MainDatabaseHelper extends SQLiteOpenHelper {

    /*
     * Instantiates an open helper for the provider's SQLite data repository
     * Do not do database creation and upgrade here.
     */
    MainDatabaseHelper(Context context) {
        super(context, DBNAME, null, 1);
    }

    /*
     * Creates the data repository. This is called when the provider attempts to open the
     * repository and SQLite reports that it doesn't exist.
     */
    public void onCreate(SQLiteDatabase db) {

        // Creates the main table
        db.execSQL(SQL_CREATE_MAIN);
    }
}-

实现ContentProvider的MIME

ContentProvider有两个方法返回MIME:

1.getType()
必须实现的方法

2.getStreamTypes()
如果提供文件访问就需要实现这个方法。

表的MIME

getType方法会返回一个MIME格式的string,表明了当前 content URI 参数返回的数据的类型。Uri参数可以是一个规则而不局限于一个具体的URI.这时,应该返回匹配这个规则的URI的数据类型。

对于常见的数据类型,比如text,html,jpeg,getType应该返回标准的MIME。在IANA MIME Media Types的官网上列举了常用的数据类型。

对于指定一行或者多行的的content URI,getType应该返回一个系统的vendor-specific格式的MIME:

类型部分:vnd

子部分:
如果是一行的,android.cursor.item/
如果是多行的,android.cursor.dir/

provider的特有部分: vnd..

我们提供. 应该是唯一的,在对应的URI类型中是唯一的。可以是公司的名称或者app的包名。可以是这个URI关联的表名。

比如,如果provider的authority是com.example.app.provider,它显露出来了一个table1的表,MIME是:

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

对于table1有一行,MIME是:

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

文件的MIME

如果我们的provider提供文件,需要实现getStreamTypes方法。这个方法会返回一个文件的MIME字符数组,这些文件是通过一个content URI从provider中得到的。我们应该通过MIME的过滤参数来过滤我们提供的MIME, 这样我们就可以返回客户端需要的MIME.

比如,有一个provider提供图片文件,格式可以是.jpg,.png和.gif格式。如果一个app调用ContentResolver.getStreamTypes(),传入参数image/*,这时ContentResolver.getStreamTypes()应该返回数组:
{ "image/jpeg","image/png","image/gif"}

如果app只想要.jpg文件,那么可以调用ContentResolver.getStreamTypes(),传入参数*/jpeg,ContentResolver.getStreamTypes()应该返回:{"image/jpeg"}

如果provider没有提供任何的MIME参数,getStreamTypes()应该返回null.

实现一个协定类

协定类是一个final的class,包含了URI,列名,MIME和其它provider的元数据。它建立了provider和其它app的一个协定,即使URI,列名等发生了改变,provider还是能通过它被访问。

协定类对开发者来说也是非常有用的,因为它使用的是名字都是方便记忆的,所以开发者在使用时更不容易发生错误。因为它是一个类,所以可以有java文档注释。像Android Studio这样的IDE可以从协定类中自动补全并展示javadoc.

开发者不可以在自己的app中访问这些协定类,但是可以直接把.jar包打包到自己的app中。

ContactsContract和它的嵌套类是典型的协定类。

实现content provider的权限

权限和访问请求在Security and Permissions中有详细的描述。Data Storage 也对不同类型的存储做了安全和权限相关的讲解。简单来说,重要的是:

  1. 默认的,在设备内部存储的文件对于我们的app和provider来说都是私有的。
  2. 我们创建的SQLiteDatabase对我们的app和prvoider也是私有的。
  3. 默认的存储到公共存储的文件都是公有的,全局可写的。不能使用content prvoider来限制对公共区域的文件的访问,因为其它的app可以使用其它的api来读写这些文件。
  4. 那些方法请求可以在我们的内部区域打开或者创建文件或SQLite 数据库是有风险的,它们可以给其它的app读写权限。如果我们使用内部文件或者数据库作为provider的仓库,我们给他们设定为全局读写的权限。我们在清单文件中设定的权限是不能保护我们的数据。默认访问内部文件和数据库是私有的,我们不能改变这个原则。

如果我们想要使用权限来管理provider的数据,我们应该把在内部文件或SQLite中存储数据,或者上传到服务器。我们应该保证文件和数据库对app的私有。

实现权限

即使底层数据是私有的,所有的app都应该可以从provider中读取数据和写入数据,因为默认的proivder是没有权限的。为了这种情况,在清单文件中设置权限,修改的属性或添加子节点。我们可以给整个provider设置权限,也可以为一张表或者一条记录,或者全部设置权限。

可以在清单文件中使用一个或多个来定义权限。为了让权限唯一,使用java风格的作用域来给android:name属性赋值。比如,定义读的权限:com.example.app.provider.permission.READ_PROVIDER.

下边的列表展示了provider的权限作用域,先是对整个provider的,然后逐渐细化。细化的优化级更高。

provider级别的统一读写权限

一个控制对整个provider读写的权限,在节点属性 android:permission

provider级别的独立读写权限

整个provider的读权限和写权限。我们可以使用 的属性android:readPermission和android:writePermission,他们的优先级比android:permissiong更高。

路径级别的权限

provider的一个content path的读权限,写权限或者读写权限。可以为每一个URI指定权限,在 添加一个子节点 . 然后指定读写权限,读权限或者写权限,或者全部。读或者写的权限优先于读写权限。同时,路径级别的权限优先于provider级别的权限。

临时权限

即使app没有权限,它还是可以得到一个临时权限。临时权限可以减少清单文件中声明的权限数量。当我们开启临时权限,只有那些需要持续访问数据的才需要永久权限。

比如,当我们想要外边的图片查看器来展示附件的图片,我们想要实现一个邮件provider和app的权限。为了让图片查看器不请求权限还能进行必要的访问,为这个图片的content URI设定一个临时权限。设计我们的邮件app,当用户想要展示一个图片时,app发送一个intent给图片查看器,intent包含了图片的content URI和权限的flag. 图片查看器然后可以查询emial的provider得到数据,即使它没有读取的权限。

为了能打开临时权限,要么设置的属性android:grantUriPermissions或者添加一个或多个 的子节点。 如果使用临时权限,oontent URI关联了临时权限,当我们想要移除对一个content URI的支持时,我们需要调用 Context.revokeUriPermission()。

属性的值决定了provider的访问范围。如果属性是true,那么系统会为整个provider授予临时权限,会重写掉我们声明的其它prvoider级别或者路径级别的权限。

如果这个flag是false,我们需要给添加子节点 . 每个子节点指定需要临时权限的一个content URI或者多个URI.

为了给一个app临时的访问权限,intent必须有一个FLAG_GRANT_READ_URI_PERMISSION或者FLAG_GRANT_WRITE_URI_PERMISSION的标记,或者两个都有。都是通过setFlags方法来设定的。

android:grantUriPermissions 默认是false.

节点

和Acitivity,Service组件类似,ContentProvidder也必须在清单文件中声明,使用节点。系统会从该元素中获取以下信息:

Authority (android:authorities)
在系统中的名字标识。参阅Designing Content URIs

Provider class name ( android:name )
实现ContentProvider的类名。参阅Implementing the ContentProvider Class

Permissions
指定其它app访问provider数据需要的权限:
android:grantUriPermssions: 临时权限.
android:permission: 统一读写权限.
android:readPermission: 读权限.
android:writePermission: 写权限.
参阅Implementing Content Provider Permissions

Startup and control attributes
这些属性决定了系统何时和以何种方式来启动provider,provider的进程特性和运行时设置:
android:enabled: 允许系统启动provider的标记
android:exported: 允许其它app启动provider的标记
android:initOrder: provider的启动顺序,相对于同一个进程中的其它prvoider.
android:multiProcess:允许系统在客户端的进程中启动provider的标记
android:process: prvoider运行的进程名
android:syncable: 表示provider的数据是否应该和服务器的数据同步

参阅

Informational attributes
provider一个可选的图标和标题
android:icon: provider的图标,会在Settings>APP>ALL的app列表中出现。
android:label: provider的标题,会在Settings>APP>ALL的app列表中出现。

参阅

Intent和数据访问

app可以通过intent来间接访问content provider. app不调用 ContentResolver或者ContentProvider的方法,而是发送一个intent来启动一个activity,这个activity是provider app的一部分。目标activity控制数据的访问和展示。根据intent的action,目标activity也可以提示用户来修改数据。intent也可以包含"extras"展示到目标activity的UI,用户可以修改它,并用它来修改provider的数据。

我们也可以使用intent来保证数据的完整性。 provider可能根据严格的逻辑来插入数据,更新数据,或者删除数据。如果是这样,让其它的app直接修改我们的数据可能会产生无效数据。如果想要求开发者使用intent来访问,请提供详细的文档。解释清楚为什么使用intent比直接使用代码修改数据要好。

修改provider数据的intent和处理其它的intent类似。参阅Intents and Intent Filters

你可能感兴趣的:(Creating a Content Provider)