Android 四大组件(三)ContentProvider

这篇主要介绍下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

欢迎关注朋永的简书!


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

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,569评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 素颜的我皮肤很不好,但我很是喜欢素颜的状态。 没有负担的皮肤,没有负担的心情。 早上醒的其实不迟,看书看到头痛,然...
    秋落巴士阅读 108评论 0 1
  • 昨晚突然想看电影。《三傻大闹宝莱坞》看了几分钟,终究是看不下去,虽然评价很高。本来想看《哆啦A梦,与我同行》,可是...
    萧澄阅读 246评论 4 4
  • 易为-男-200907 #90条目标汇总# 孩子目标: (1)作息规律:周日-周四:晚9:15关灯早6:55起床;...
    玲儿311阅读 143评论 0 0