写一个ContentProvider的实用案例

引言

在安卓安全模型中,一个应用程序不能够直接去访问(读或写)另一个应用程序的数据,每个应用程序都有其自己的带id的结构化数据,自己的文件夹和私有的内存区域,ContentProvider是在这些应用程序间件共享数据。ContentProvider是一组包裹在标准的API下的读写操作。应用程序都必须注册为数据提供者才能共享数据,其它的应用程序能够通过固定的API去读写这些数据,所以ContentProvider严格遵守CRUD原则。
常见的实用ContnentProvider的案例有读取联系人方式、读取多媒体信息、以及其它的应用程序数据,同时也允许这些应用程序增删改查这些数据。

上图示意了ContentProvider是如何工作的,App1储存它的数据到其自己的数据库并且提供提供一个provider,另一个应用程序App2通过这个共享出来的provider曲获取App1的数据。ContentProvider是一种简单抽象的接口,使用了标准的insert(),query(),update(),delete()方法来获取和更新数据,可见实现一个ContentProvider是非常简单的。

每个ContentProvider都会被冠以content://字符串开头的作为特殊标识的URI,这是用来区分不同的应用程序的。

如何定义一个ContentProvider

作为安卓最重要的组建之一,要定义一个ContentProvider我们主要有如下步骤:

  1. 建立一个继承自ContentProvider的类
  2. 定义一个特殊的URI
  3. 实现所有的insert(), update(), query(), delete(), getType().方法
  4. 在AndroidManifest.xml中去声明
定义URI:

一个ContentProvider的URI定义的时候主要包括四部分.
比如:content://authority/path/id
content:// 所有的ContentProvider的URI定义都要以它打头
‘authority’ ContentProvider实现的时候的包名路径
‘path’ 这是一个虚拟的路由用来标识App下的某些数据的映射
‘id’ 可选字段,用来标识数据中的某一条信息

增加新的纪录:

增加一条心得纪录到数据库我们需要复写ContentProvider的insert()方法。调用者必须要指定ContentProvider的URI和想要传的值,但是不必传ID。成功插入的花会返回URI和新插入id的值。

比如:如果我们插入一条记录到content://com.example/sample,那么会返回content://com.example/sample/1。

更新纪录:

通过ContentProvider更新一条或多条数据,我们首先需要指定Content Provider的URI,通过update()方法去更新。可以通过指定id去更新某一条数据,但是如果要更新多条数据,就必须要传入selection参数,表示哪些行要被改变。该方法会返回影响的行数。

删除记录:

删除一条或多条数据和更新数据非常类似,我们需要指定id或者selection来进行删除。Content Provider的delete()方法会返回删除的记录的条数。

通过ContentProvider查询记录:

要通过ContentProvider去查询数据我们需要复写query()方法,这个方法有多个参数,可以通过projection参数指定列,通过selection指定筛选标准,还可以指定sortOrder,如果不指定projection,指针的所有列的值都会返回,如果不指定排序,那么会返回数据库中查询出来的默认排序。

getType()方法:

该方法用来处理指定URI后的MIME类型数据的请求,我们可以使用vnd.android.cursor.item或者vnd.android.cursor.dir/。 vnd.android.cursor.item用来指定某一个项目,而另一个用来指定所有的文件.

在AndroidManifest.xml中注册ContentProvider

作为四大组件之一,要使用的时候当然得在AndroidManifest.xml中去注册。 标签用来注册,包含在标签中.比如:

1
2
3
4
<provider
android:name=".MyProvider"
android:authorities="com.example.contentproviderexample.MyProvider">
</provider>

这里authorities是用来获取ContentProvider的,一般使用该类的路径。

一个例子

决定动手写一个ContentProvider的例子,我们这个例子包含两个Application,一个应用用来生成数据,另外一个应用用来获取前一个应用生成的数据。

共享数据的应用

第一个产生数据的应用很简单,就输入一个名字然后存到sqlite的db里面,成功就提示插入成功,同时可以点击查询按钮获取已经存好的姓名列表。新建一个名叫gordon_database的数据库,新建表names,拥有id和name两列值。
应用ContentProvider的URI是rawe.gordon.com.understandcontentprovider.provider.GordonContentProvider/gordon,首先来看看布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/input_one_name"
android:textColor="#000000" />
<EditText
android:id="@+id/input_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp" />
<TextView
android:id="@+id/sure"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/corner_2_background"
android:paddingBottom="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:paddingTop="10dp"
android:text="@string/sure" />
<TextView
android:id="@+id/query"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/corner_2_background"
android:paddingBottom="10dp"
android:paddingLeft="20dp"
android:paddingRight="20dp"
android:paddingTop="10dp"
android:text="@string/query" />
</LinearLayout>

然后是我们的测试Activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private EditText editText;
private TextView sureView;
private TextView queryView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editText = (EditText) findViewById(R.id.input_name);
sureView = (TextView) findViewById(R.id.sure);
queryView = (TextView) findViewById(R.id.query);
sureView.setOnClickListener(this);
queryView.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.query) {
StringBuilder retValue = new StringBuilder();
String sortOrder = "id";//可以尝试"name"
Cursor cursor = getContentResolver().query(ServerProvider.CONTENT_URI, null, "", null, sortOrder);
if (cursor == null) return;
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
String content = cursor.getString(cursor.getColumnIndex(ServerProvider.KEY_FIELD_NAME));
int id = cursor.getInt(cursor.getColumnIndex("id"));
retValue.append(String.valueOf(id)).append(" ").append(content).append("\n");
cursor.moveToNext();
}
Toast.makeText(getBaseContext(), retValue, Toast.LENGTH_LONG).show();
} else if (v.getId() == R.id.sure) {
ContentValues values = new ContentValues();
values.put(ServerProvider.KEY_FIELD_NAME, editText.getText().toString().trim());
Uri uri = getContentResolver().insert(ServerProvider.CONTENT_URI, values);
if (uri != null) {
Toast.makeText(getBaseContext(), "成功插入一条纪录 -> " + uri.toString(), Toast.LENGTH_LONG).show();
}
}
}
}

最关键的ContentProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/**
* Created by gordon on 6/9/16.
*/
public class ServerProvider extends ContentProvider {
private SQLiteDatabase db;
static final String DATABASE_NAME = "gordon_database";
static final String TABLE_NAME = "names";
static final int DATABASE_VERSION = 1;
public static final String KEY_FIELD_NAME = "name";
static final String CREATE_DB_TABLE = " CREATE TABLE " + TABLE_NAME + " (id INTEGER PRIMARY KEY AUTOINCREMENT, " + KEY_FIELD_NAME + " TEXT NOT NULL);";
public static final String AUTHORITY = "app.server.com.provider.ServerProvider";
public static final String URL_SUFFIX = TABLE_NAME;
public static final String SCHEMA = "content://";
public static final String URL = SCHEMA + AUTHORITY + "/" + URL_SUFFIX;
public static final Uri CONTENT_URI = Uri.parse(URL);
public static final String DEFAULT_TYPE = "vnd.android.cursor.dir";
public static final int URI_MATCH_SUCCESS_CODE = 1;
public static final UriMatcher uriMatcher;
private Context context;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, URL_SUFFIX, URI_MATCH_SUCCESS_CODE);
uriMatcher.addURI(AUTHORITY, URL_SUFFIX + "/*", URI_MATCH_SUCCESS_CODE);
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
int count;
switch (uriMatcher.match(uri)) {
case URI_MATCH_SUCCESS_CODE:
count = db.delete(TABLE_NAME, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
context.getContentResolver().notifyChange(uri, null);
return count;
}
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case URI_MATCH_SUCCESS_CODE:
return DEFAULT_TYPE + "/" + URL_SUFFIX;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
long rowID = db.insert(TABLE_NAME, "", values);
if (rowID > 0) {
Uri _uri = ContentUris.withAppendedId(CONTENT_URI, rowID);
context.getContentResolver().notifyChange(_uri, null);
return _uri;
}
throw new SQLException("Failed to add a record into " + uri);
}
@Override
public boolean onCreate() {
context = getContext();
DatabaseHelper dbHelper = new DatabaseHelper(context);
db = dbHelper.getWritableDatabase();
return db != null;
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(TABLE_NAME);
switch (uriMatcher.match(uri)) {
case URI_MATCH_SUCCESS_CODE:
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = KEY_FIELD_NAME;
}
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
c.setNotificationUri(context.getContentResolver(), uri);
return c;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count;
switch (uriMatcher.match(uri)) {
case URI_MATCH_SUCCESS_CODE:
count = db.update(TABLE_NAME, values, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
context.getContentResolver().notifyChange(uri, null);
return count;
}
private static class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_DB_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
onCreate(db);
}
}
}

AndroidManifest.xml中声明:

1
2
3
4
5
<provider
android:name=".provider.ServerProvider"
android:authorities="app.server.com.provider.ServerProvider"
android:exported="true"
android:multiprocess="true" />

好了,就是这么简单,当然有几个参数要注意一下,我们在manifest.xml中声明的authorities一定要和我们定义的类中的AUTHORITY一致,不然会抛出异常的。
演示一下,首先是输入界面:


新加一个名字:

查询我们已经插入的数据:

好了,到此一个最简单的插入和查询的ContentProvider写好了,也许你会说,这并没有什么卵用,我用Preference分分钟也搞定,存数据嘛,我有一万种方法。好了,的确如此,但是ContentProvider还可以给别的共享数据,当然是首先我们的App要允许啊,这个可以通过exported属性来设置,默认是false,那么我们把它置为true就可以了。

用别人的数据的应用

首先上布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical">
<Button
android:id="@+id/retrieve"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/display" />
<TextView
android:id="@+id/result_area"
android:gravity="center"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="false"
android:ems="10" />
</LinearLayout>

我们使用LoaderManager来查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {
private Button retrieve;
private TextView displayContent;
public static final String NAMES_URL = "content://app.server.com.provider.ServerProvider/names";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
retrieve = (Button) findViewById(R.id.retrieve);
displayContent = (TextView) findViewById(R.id.result_area);
retrieve.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//触发查询操作
getSupportLoaderManager().initLoader(1, null, MainActivity.this);
}
});
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(this, Uri.parse(NAMES_URL), null, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
cursor.moveToFirst();
StringBuilder res = new StringBuilder();
while (!cursor.isAfterLast()) {
res.append("\n" + cursor.getString(cursor.getColumnIndex("id")) + "-" + cursor.getString(cursor.getColumnIndex("name")));
cursor.moveToNext();
}
displayContent.setText(res);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
}

最重要的是我们要知道别的App的export出来的schema,这样才能正常的增删改查,同时也得知道数据结构是怎样的,列名是什么。
好了,来看看结果:

总结

使用ContentProvider还是很好的,可以很方便的玩转数据,复杂的操作只是在现在的基础上进行改进就行了,好了,写到这,如果想要试用ContentProvider,可以考虑从我下面的git地址去拷贝一份吧。

不想敲代码就到我的git来拉一下吧~https://github.com/gordon-rawe/understandContentProvider