Android 四大组件(三)ContentProvider

96
朋永
2017.07.19 20:23* 字数 4280

这篇主要介绍下ContentProvider如何实现共享数据、及ContentResolver如何访问其他进程等数据。


ContentProvider共享数据

简介

ContentProvider管理对一组结构化数据的访问。 它们封装了数据,并提供了定义数据安全性的机制。 ContentProvider是将一个进程中的数据与在另一进程中运行的代码相连接的标准接口。

当要访问ContentProvider中的数据时,您可以使用应用程序上下文中的ContentResolver对象作为客户端与提供者进行通信。 ContentResolver对象与Provider对象进行通信,该对象是实现ContentProvider的类的一个实例。提供者对象从客户端接收数据请求,执行请求的操作,并返回结果。

如果您不打算与其他应用程序共享数据,则不需要开发自己的provider程序。但是,您需要自己的provider在您自己的应用程序中提供自定义搜索建议。如果要将应用程序中的复杂数据或文件复制并粘贴到其他应用程序,还需要自己的provider程序。

Android本身包括管理音频,视频,图像和个人联系信息等数据的ContentProvider。有一些限制,任何Android应用程序都可以访问这些提供程序。

在开始构建提供程序之前,请先决定是否需要ContentProvider。如果要提供一个或多个以下功能,则需要构建内容提供程序:

  1. 你想提供复杂的数据或文件到其他应用程序。
  2. 您希望允许用户将应用程序中的复杂数据复制到其他应用程序中。
  3. 您希望使用搜索框架提供自定义搜索建议。

如果使用的完全是你自己应用程序中的SQLite数据库,则并不需要一个ContentProvider。

接下来,按照以下步骤构建ContentProvider:

  1. 为您的数据设计原始存储。ContentProvider以两种方式提供数据:
  • 文件数据
    通常是文件的数据,如照片,音频或视频。将文件存储在应用程序的私有空间中。响应来自另一个应用程序的文件请求,您的ContentProvider可以提供该文件的句柄。
  • “结构化”数据
    通常进入数据库,数组或类似结构的数据。以与行和列的表格兼容的表单存储数据。一行代表一个实体,例如一个人或一个库存中的一个物品。列表示实体的某些数据,例如某人的名称或项目的价格。存储此类型数据的常见方法是在SQLite数据库中,但您可以使用任何类型的永久存储。
  1. 定义ContentProvider类及其所需方法的具体实现。
  2. 定义ContentProvider的authority字符串,其内容URI和列名称。如果您希望提供程序的应用程序处理意图,还可以定义intent action,extras data和flags。还要定义要访问数据的应用程序所需的权限。您应该考虑将所有这些值定义为单独的合同类中的常量; 稍后,您可以将此类暴露给其他开发人员。

数据存储这里就不说了,不了解数据库的可以参考我这篇blog:Android 数据存储 (三)SQLite Databases

Content URIs

Content URI是标识provider中的数据的URI。 Content URI包括整个provider(其权限)的符号名称和指向表(路径)的名称。 可选的ID部分指向表中的单个行。每个数据访问方法 ContentProvider都有一个内容URI作为参数; 这允许您确定要访问的表,行或文件。
uri格式:

content://com.example.app.provider/table

分为三个部分:

  • content:// 这部分是Android规定,固定的。
  • com.example.app.provider 这个部分是ContentProvider的authority。系统是根据这个部分来找到操作哪个ContentProvider。
  • table 数据部分,访问不同资源时,这个部分是动态改变的。例如,如果您有两个表table1和 table2,则将该部分设置为table1和table2。

内容URI模式使用通配符匹配内容URI:

  • *: 匹配任何长度的任何有效字符的字符串。
  • #: 匹配任意长度的数字字符串。

uri功能丰富,如下
content://com.zpengyong.app.provider/table/1 访问table数据中id为1的记录。
content://com.zpengyong.app.provider/table/1/name 访问table数据中id为1的name字段。

实现ContentProvider类

ContentProvider实例通过处理来自其他应用程序的请求来管理对一组结构化数据的访问。所有形式的访问最终都会调用ContentResolver,然后调用一个具体的方法ContentProvider来获取访问权限。

必需的方法
抽象类ContentProvider定义了您必须实现的六个抽象方法,作为您自己的具体子类的一部分。所有这些方法除了 onCreate()被客户端应用程序调用尝试访问您的ContentProvider。

  • query()
    从ContentProvider检索数据。
    该方法必须返回一个Cursor对象,如果失败则会抛出异常。如果您使用SQLite数据库作为数据存储,则可以简单地返回由SQLiteDatabase类的一个query()方法返回的Cursor。如果查询与任何行都不匹配,应该返回Cursor实例(其getCount()方法为0)。只有在查询过程中发生内部错误时,才应返回null。如果不使用SQLite数据库作为数据存储,请使用Cursor的具体子类之一。例如,MatrixCursor类实现一个游标,其中每行都是Object的数组。使用这个类,使用addRow()来添加一个新行。
  • insert()
    在ContentProvider中插入一行。使用ContentValues参数中的值将新行添加到适当的表。 如果列名不在ContentValues参数中,则可能需要在provider程序代码中或数据库模式中为其提供默认值。 此方法应返回新行的URI。 要构造它,使用withAppendedId()将新行的_ID(或其他主键)值附加到表的URI。
  • update()
    更新ContentProvider中的现有行。使用参数来选择要更新的表和行,并获取更新的列值。返回更新的行数。
  • delete()
    从ContentProvider中删除行。使用参数选择要删除的表和行。返回删除的行数。
  • getType()
    返回与内容URI对应的MIME类型。在“ 实现内容提供者MIME类型 ”部分中更详细地描述了该方法。
  • onCreate()
    初始化ContentProvider。Android系统在创建您的ContentProvider后立即调用此方法。请注意,在ContentResolver对象尝试访问它之前,provider程序不会被创建 。

实现这些方法应该说明如下:

  • 除onCreate()之外的所有这些方法都可以由多个线程同时调用,因此它们必须是线程安全的。
  • 避免在onCreate()中执行长时间的操作。 延迟初始化任务,直到实际需要。
  • 虽然您必须实现这些方法,但您的代码除了返回预期的数据类型之外,不必执行任何操作。 例如,您可能希望防止其他应用程序将数据插入某些表。 为此,您可以忽略对insert()的调用并返回0。

Android系统在启动ContentProvider程序时调用onCreate()。 您应该在此方法中仅执行快速运行的初始化任务,并延迟数据库创建和数据加载,直到provider程序实际接收到对数据的请求。 如果在onCreate()中执行冗长的任务,您将减慢provider的启动速度。 反过来,这将减慢从provider到其他应用程序的响应。
例如,如果使用SQLite数据库,则可以在ContentProvider.onCreate()中创建一个新的SQLiteOpenHelper对象,然后在第一次打开数据库时创建SQL表。 为了方便起见,第一次调用getWritableDatabase()时,它会自动调用SQLiteOpenHelper.onCreate()方法。

数据存储

数据存储效果:

  • 默认情况下,存储在设备内部存储上的数据文件对您的应用程序和provider程序是私有的。
  • 您创建的SQLiteDatabase数据库对您的应用程序和provider程序是私有的。
  • 默认情况下,您保存到外部存储的数据文件是公共的和可读的。您不能使用内容提供商限制对外部存储中的文件的访问,因为其他应用程序可以使用其他API调用来读取和写入它们。
  • 该方法调用在设备内部存储上打开、创建文件或SQLite数据库 可能会给所有其他应用程序的读取和写入访问。如果您使用内部文件或数据库作为ContentProvider的存储库,并将其提供为"world-readable" or "world-writeable"访问权限,那么在其清单中为您的provider设置的权限将不会保护您的数据。内部存储中的文件和数据库的默认访问权限为“私有”,对于provider的存储库,您不应该更改此权限。

如果要使用ContentProvider权限来控制对数据的访问,则应将数据存储在内部文件,SQLite数据库或“云”(例如远程服务器)中,并且应保留文件和数据库对你的应用程序是私有的。

权限

即使底层数据是私有的,所有应用程序都可以读取或写入您的ContentProvider程序,因为默认情况下,您的ContentProvider程序没有设置权限。 要更改此设置,请使用<provider>元素的属性或子元素为清单文件中的ContentProvider设置权限。 您可以设置适用于整个ContentProvider程序,特定表格,甚至某些记录或三者的权限。
您的清单文件中包含一个或多个<permission>元素,为您的ContentProvider定义权限。 要使您的ContentProvider程序唯一的权限,请使用Java风格的范围设置为android:name属性。 例如,将读取权限命名为com.example.app.provider.permission.READ_PROVIDER。

以下描述了ContentProvider程序权限的范围,从适用于整个ContentProvider程序的权限开始,然后变得更细。 更细的权限优先于较大范围的权限:

  • Single read-write provider-level permission(单一读写提供程序级)
    该权限控制对整个ContentProvider的读取和写入访问,由<provider>元素的android:permission属性指定。
  • Separate read and write provider-level permission(单独的读写提供程序级)
    对整个ContentProvider的读取权限和写入权限。您可以使用<provider>元素的属性 android:readPermission和 android:writePermission属性来 指定它们。它们优先于android:permission。
  • Path-level permission(路径级)
    ContentProvider的URI的读取、写入、读取/写入权限。可以使用<provider>元素的<path-permission>子元素指定要控制的每个URI。 对于您指定的每个内容URI,您可以指定读/写权限,读权限或写权限,或全部三个。 读权限、写权限优先于读/写权限。 此外,路径级权限优先于provider级权限。
  • Temporary permission(临时)
    即使应用程序没有通常需要的权限,也允许临时访问应用程序的权限级别。临时访问功能减少应用程序在其清单中的权限数量。当您启用临时权限时,只有您的provider需要“永久”权限的应用程序才能持续访问您的所有数据。
    当您希望允许外部图像查看器应用程序从您的提供商显示照片附件时,请考虑实施电子邮件提供商和应用程序所需的权限。为了给图像查看器提供必要的访问权限,而不需要权限,请为照片的内容URI设置临时权限。设计您的电子邮件应用程序,以便用户想要显示照片时,应用程序会向图像查看器发送包含照片的内容URI和权限标志的意图。然后,图像查看器可以查询您的电子邮件提供商以检索照片,即使查看器对您的提供商没有正常的读取权限。
    要启用临时权限,可以设置<provider>元素的android:grantUriPermissions属性,或者在<provider>元素中添加一个或多个<grant-uri-permission>子元素。如果您使用临时权限,则每当您从提供程序中删除对内容URI的支持时,都必须调用Context.revokeUriPermission(),并且内容URI与临时权限相关联。
    该属性的值决定了您的提供者的访问量。如果属性设置为true,则系统将向整个提供程序授予临时权限,覆盖您的提供程序级别或路径级权限所需的任何其他权限。
    如果此标志设置为false,则必须在<provider>元素中添加<grant-uri-permission>子元素。每个子元素指定授予临时访问权限的内容URI或URI。
    要临时访问应用程序,intent必须包含FLAG_GRANT_READ_URI_PERMISSION或FLAG_GRANT_WRITE_URI_PERMISSION标志,或两者都包含。这些都是用setFlags()方法设置的。
    如果android:grantUriPermissions属性不存在,则假定为false。

<provider>元素

像Activity和Service组件一样,子类ContentProvider 必须使用该 <provider>元素在其应用程序的清单文件中定义 。Android系统从元素获取以下信息:
权限(android:authorities)
 标识系统中整个提供商的符号名称。
提供者类名( android:name)
 实现的类ContentProvider。该类在“ 实现ContentProvider类 ”一节中有更详细的描述 。
权限
 指定其他应用程序必须具有访问提供程序数据的权限的属性:

  • android:grantUriPermssions:临时许可标志。
  • android:permission:单提供商范围的读/写权限。
  • android:readPermission:提供商范围的读取权限。
  • android:writePermission:提供商范围的写入许可。

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

  • android:enabled:允许系统启动提供程序的标志。
  • android:exported:允许其他应用程序使用此提供程序的标志。
  • android:initOrder:相对于同一进程中的其他提供者,应启动此提供程序的顺序。
  • android:multiProcess:允许系统以与调用客户端相同的过程启动提供程序的标志。
  • android:process:运行提供程序的进程的名称。
  • android:syncable:表示提供者的数据要与服务器上的数据同步的标志。

信息属性
 provider的可选图标和标签:

  • android:icon:包含provider的图标资源。“ 设置” >“ 应用程序” >“ 全部”中,应用程序列表中提供商标签旁边会显示该图标 。
  • android:label:描述提供者或其数据的信息标签,或两者。标签会显示在“ 设置” >“ 应用程序” >“ 全部”中的应用列表中 。

代码

效果:


ContentProvider 应用

这个是基于之前数据库的demo改的。主界面主要是数据库的相关操作。
ContentProvider代码如下:

package com.zpengyong.app;

import com.zpengyong.app.db.UserSQLiteOpenHelper;

import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;

public class TestProvider extends ContentProvider {
    private static final String TAG = "TestProvider";
    
    private static final String CONTENT_AUTHORITY = "com.zpengyong.app.provider";
    private static final String PATH_USER = "/user";
    private static final int CODE_USER = 1;
    
    private static final Uri URI_USER = Uri.parse("content://com.zpengyong.app.provider/user");
    
    private static final UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    static{
        mUriMatcher.addURI(CONTENT_AUTHORITY, PATH_USER, CODE_USER);        
    }
    private Context mContext;

    private UserSQLiteOpenHelper mUserSQLiteOpenHelper;
    private SQLiteDatabase mDatabase;
    
    private ContentResolver mContentResolver;
    @Override
    public boolean onCreate() {
        mContext = getContext(); //获取上下文
        mUserSQLiteOpenHelper = UserSQLiteOpenHelper.getInstance(mContext); 
        mDatabase = mUserSQLiteOpenHelper.getWritableDatabase();
        mContentResolver = mContext.getContentResolver();
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        int match = mUriMatcher.match(uri);
        Cursor result;
        switch(match){
        case CODE_USER:
            result = mDatabase.query(UserSQLiteOpenHelper.DATABASE_TABLE_USER, 
                    projection, selection, selectionArgs, null, null, sortOrder);
            break;
        default:
            throw new UnsupportedOperationException("query not supported on uri:"+uri);
        }
        return result;
    }

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

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        int match = mUriMatcher.match(uri);
        Uri result;
        switch(match){
        case CODE_USER:
            long id = mDatabase.insert(UserSQLiteOpenHelper.DATABASE_TABLE_USER, null, values);
            result = ContentUris.withAppendedId(uri, id);
            break;
        default:
            throw new UnsupportedOperationException("Insert not supported on uri:"+uri);
        }
        if(result != null){
            //通知数据库的变化
            mContentResolver.notifyChange(URI_USER, null);
        }
        return result;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int match = mUriMatcher.match(uri);
        int result;
        switch(match){
        case CODE_USER:
            result = mDatabase.delete(
                    UserSQLiteOpenHelper.DATABASE_TABLE_USER, 
                    selection, selectionArgs);
            break;
        default:
            throw new UnsupportedOperationException("delete not supported on uri:"+uri);
        }
        if(result > 0){
            mContentResolver.notifyChange(URI_USER, null);
        }
        return result;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        int match = mUriMatcher.match(uri);
        int result;
        switch(match){
        case CODE_USER:
            result = mDatabase.update(
                    UserSQLiteOpenHelper.DATABASE_TABLE_USER, 
                    values, selection, selectionArgs);
            break;
        default:
            throw new UnsupportedOperationException("Update not supported on uri:"+uri);
        }
        if(result > 0){
            mContentResolver.notifyChange(URI_USER, null);
        }
        return result;
    }
}

ContentResolver访问共享数据

先看效果图:


ContentResolver 应用

该应用通过ContentResolver访问另一个应用ContentProvider中的共享数据,并就行删除、添加、更改操作。监听数据库等变化更新UI界面。

1 访问数据库:

private void refreshList() {
    // 查询数据,更新到listview上
    Cursor cursor = mContentResolver.query(URI_USER, null, null, null, "modifyTime desc");
    if (null != cursor) {
        List<UserBean> userList = new ArrayList<UserBean>();
        while (cursor.moveToNext()) {
            UserBean user = new UserBean();
            user.set_id(cursor.getLong(cursor.getColumnIndex(COL_ID)));
            user.setName(cursor.getString(cursor.getColumnIndex(COL_NAME)));
            user.setPwd(cursor.getString(cursor.getColumnIndex(COL_PWD)));
            user.setModifyTime(cursor.getLong(cursor.getColumnIndex(COL_TIME)));
            userList.add(user);
        }
        mUserDataList = userList;
        // 刷新listview
        mAdapter.notifyDataSetChanged();
        cursor.close();
    }
}

2 添加数据

ContentValues values = new ContentValues();
values.put(COL_NAME, name);
values.put(COL_PWD, pwd);
values.put(COL_TIME, System.currentTimeMillis());
// 添加数据
Uri uri = mContentResolver.insert(URI_USER, values);

3 删除数据

// 删除数据 返回值表示删除的行数
int result = mContentResolver.delete(URI_USER, "_id = ?", new String[] { id + "" });

4 修改数据

String name = nameEt.getText().toString();
String pwd = pwdEt.getText().toString();
if (pwd.length() > 0 || name.length() > 0) {
    long id = mUserDataList.get(position).get_id();
    ContentValues values = new ContentValues();
    if (name.length() > 0) {
        values.put(COL_NAME, name);
    } else {
        values.put(COL_PWD, pwd);
    }
    values.put(COL_TIME, System.currentTimeMillis());
    // 修改数据 返回值表示修改的行数
    int result = mContentResolver.update(URI_USER, values, "_id = ?", new String[] { id + "" });
    Log.i(TAG, "update id=" + id + ",result=" + result);
}

5 监听数据库变化

mContentObserver = new MyContentObserver(new Handler());
mContentResolver = mContext.getContentResolver();
//监听数据库的变化
mContentResolver.registerContentObserver(URI_USER, true, mContentObserver);

class MyContentObserver extends ContentObserver {
    public MyContentObserver(Handler handler) {
        super(handler);
    }

    // 数据库变化会回调该方法
    @Override
    public void onChange(boolean selfChange) {
        super.onChange(selfChange);
        Log.i(TAG, "onChange selfChange=" + selfChange);
        refreshList();
    }
}

6 取消监听数据库变化

// 取消监听数据库的变化
mContentResolver.unregisterContentObserver(mContentObserver);

相关文章:
Android 四大组件(一)Activity
Android 四大组件(二)Service

欢迎关注朋永的简书!


欢迎扫一扫关注我的微信公众号,不定期推送优质技术文章:

欢迎关注
Android 基础
Web note ad 1