Android四大组件中ContentProvider组件相对Activity,BroadcastReceiver, Service而言比较独立,而且多数使用的时候都是在用Android系统提供的关于邮件,媒体,短信,联系人等ContentProvider。通常开发的应用较少提供ContentProvider组件,一般会在同家公司的产品或则内容适配方面会用到ContentProvider组件。
本文主要讲述开发和使用ContentProvider组件的通用方式。其中包含:代码模板,权限设置,ContentResolver 等。
1 . 开发ContentProvider基本思路
1.1.ContentProvider组件需要对外开放的内容
授权字符串(Authoritis),内容类型,数据字段名称, 访问权限说明等
1.2. 编写ContentProvider组件类继承ContentProvider类,实现其中的CRUD操作方法
1.3. AndroidMainifest.xml中配置<provider>元素,指定控制属性,其中包含是否对外部应用可用,读写权限,Authorities字符串等
1.4. 应用本身或者外部应用使用ContentProvider组件
2. 开发一个基于SQLite数据库提供Note表信息的ContentProvider
2.1 开发一个独立的ContentProvider组件代码框架
package secondriver.xprovider;
import android.content.*;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Author : secondriver
* Date : 2015/11/4
*/
public class XProvider extends ContentProvider {
private static final String TAG = "XProvider";
//授权字符串
//授权Uri
//Note公开信息
public static final class Note implements BaseColumns {
//Note表名
//Note表的内容Uri
//内容类型
//Note表字段
//Uri匹配码
}
//Uri匹配器
public static UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//添加Uri匹配内容
}
private XSQLiteHelper helper;
@Override
public boolean onCreate() {
//ContentProvider组件创建时做的工作
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return cursor;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return effectRows;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return effectRows;
}
public static class XSQLiteHelper extends SQLiteOpenHelper {
public static final String DATA_BASE_NAME = "xprovider.sqlite";
public static final int DATA_BASE_VERSION = 1;
public static volatile XSQLiteHelper xsqLiteHelper;
public static XSQLiteHelper getXsqLiteHelper(Context context) {
//实例化XSQLiteHelp对象
return xsqLiteHelper;
}
public XSQLiteHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
//数据库初始化
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
}
上面代码提供了一个SQLiteOpenHelper的实现,实际开发中可个会更具具体应用来提供给ContentProvider组件。
中文注释部分可能需要填充具体代码,比如权限可以更具实际情况来定义,表字段根据要提供的内容信息来公开字段名并且需要在文档中说明。
2.1 准备Note表信息
//Note公开信息
public static final class Note implements BaseColumns {
//Note表名
public static final String TABLE_NAME = "note";
//Note表的内容Uri
public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "note");
//建议使用格式如: type/subtype => vnd.android.cursor.dir/vnd.<name>.<type> name=package name type=table name
public static final String CONTENT_DIR_TYPE = "vnd.android.cursor.dir/vnd.secondriver.xprovider.note";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.secondriver.xprovider.note";
//ID主键
public static final String ID_COLUMN = _ID;
//内容列
public static final String CONTENT_COLUMN = "CONTENT";
//创建时间列表
public static final String CREATED_COLUMN = "CREATED";
//标识列
public static final String FLAG_COLUMN = "FLAG";
//状态列
public static final String STATUS_COLUMN = "STATUS";
//Uri匹配码
private static final int NOTE_ITEM = 0x21;
private static final int NOTE_DIR = 0x22;
}
说明:Uri匹配码声明为Note类的常量这样可以方便对照,一个ContentProvider组件中声明多个表公开类,比如User类,这样就比较容易区分操作的是那个表的内容。
2.2 Note类外其它的具体代码
package secondriver.xprovider;
import android.content.*;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Author : secondriver
* Date : 2015/11/4
*/
public class XProvider extends ContentProvider {
private static final String TAG = "XProvider";
//授权字符串
public static final String AUTHORITY = "secondriver.xprovider.X_PROVIDER";
//授权Uri
public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
//Uri匹配器
public static UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "note/#", Note.NOTE_ITEM);
uriMatcher.addURI(AUTHORITY, "note", Note.NOTE_DIR);
}
private XSQLiteHelper helper;
@Override
public boolean onCreate() {
helper = XSQLiteHelper.getXsqLiteHelper(getContext());
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
switch (uriMatcher.match(uri)) {
case Note.NOTE_DIR:
break;
case Note.NOTE_ITEM:
builder.appendWhere(Note.ID_COLUMN + "=" + uri.getPathSegments().get(1));
break;
default:
throw new IllegalArgumentException("Unsupported URI :" + uri);
}
builder.setTables(Note.TABLE_NAME);
Cursor cursor = builder.query(helper.getReadableDatabase(), projection, selection, selectionArgs, null,
null, sortOrder);
return cursor;
}
@Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case Note.NOTE_ITEM:
return Note.CONTENT_ITEM_TYPE;
case Note.NOTE_DIR:
return Note.CONTENT_DIR_TYPE;
default:
throw new IllegalArgumentException("Unsupported URI :" + uri);
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = helper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case Note.NOTE_DIR:
long noteId = db.insert(Note.TABLE_NAME, null, values);
if (noteId > 0) {
//插入成功
Uri newUri = ContentUris.withAppendedId(uri, noteId);
getContext().getContentResolver().notifyChange(newUri, null);
return newUri;
} else {
//插入失败
return null;
}
default:
throw new IllegalArgumentException("Unsupported URI :" + uri);
}
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = helper.getWritableDatabase();
int effectRows = 0;
switch (uriMatcher.match(uri)) {
case Note.NOTE_DIR:
effectRows = db.delete(Note.TABLE_NAME, selection, selectionArgs);
break;
case Note.NOTE_ITEM:
long id = ContentUris.parseId(uri);
String whereClause = Note.ID_COLUMN + "=" + String.valueOf(id);
if (!TextUtils.isEmpty(selection)) {
whereClause = whereClause + " AND " + selection;
}
effectRows = db.delete(Note.TABLE_NAME, whereClause, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unsupported URI :" + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return effectRows;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
SQLiteDatabase db = helper.getWritableDatabase();
int effectRows = 0;
switch (uriMatcher.match(uri)) {
case Note.NOTE_DIR:
effectRows = db.update(Note.TABLE_NAME, values, selection, selectionArgs);
break;
case Note.NOTE_ITEM:
long nodeId = ContentUris.parseId(uri);
String whereClause = Note.ID_COLUMN + "=" + String.valueOf(nodeId);
if (!TextUtils.isEmpty(selection)) {
whereClause = whereClause + " AND " + selection;
}
effectRows = db.update(Note.TABLE_NAME, values, whereClause, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unsupported URI :" + uri);
}
return effectRows;
}
public static class XSQLiteHelper extends SQLiteOpenHelper {
public static final String DATA_BASE_NAME = "xprovider.sqlite";
public static final int DATA_BASE_VERSION = 1;
public static volatile XSQLiteHelper xsqLiteHelper;
public static XSQLiteHelper getXsqLiteHelper(Context context) {
if (null == xsqLiteHelper) {
synchronized (XSQLiteHelper.class) {
if (null == xsqLiteHelper) {
xsqLiteHelper = new XSQLiteHelper(context, DATA_BASE_NAME, null, DATA_BASE_VERSION);
}
}
}
return xsqLiteHelper;
}
public XSQLiteHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.beginTransaction();
try {
db.execSQL(
new StringBuilder("create table if not exists ")
.append(Note.TABLE_NAME)
.append("(")
.append(Note.ID_COLUMN)
.append(" integer primary key autoincrement, ")
.append(Note.CONTENT_COLUMN)
.append(" varchar, ")
.append(Note.CREATED_COLUMN)
.append(" varchar,")
.append(Note.FLAG_COLUMN)
.append(" varchar,")
.append(Note.STATUS_COLUMN)
.append(" varchar")
.append(")")
.toString()
);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < 50; i++) {
ContentValues cv = new ContentValues();
cv.put(Note.CONTENT_COLUMN, "Note 内容 " + i);
cv.put(Note.CREATED_COLUMN, simpleDateFormat.format(new Date()));
cv.put(Note.FLAG_COLUMN, i);
cv.put(Note.STATUS_COLUMN, i);
db.insert(Note.TABLE_NAME, null, cv);
}
db.setTransactionSuccessful();
} catch (SQLiteException e) {
Log.e(TAG, e.getMessage());
} finally {
db.endTransaction();
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
}
3. 在AndroidMainfest.xml清单文件中声明ContentProvider组件
3.1 声明XProvider
<!-- 按如下顺序验证权限 android:grantUriPermssions: Temporary permission flag. android:permission: Single provider-wide read/write permission. android:readPermission: Provider-wide read permission. android:writePermission: Provider-wide write permission. --> <provider android:permission="secondriver.xprovider.permission.X_PROVIDER" android:authorities="secondriver.xprovider.X_PROVIDER" android:name=".XProvider" android:exported="true"> <!-- 子元素: grant-uri-permission :Uri临时访问授权 path-permission :Uri细粒度控制读写权限 --> </provider>
3.2 定义权限
清单文件中声明XProvider的时候使用到了内容读写权限
“secondriver.xprovider.permission.X_PROVIDER” 因此该权限需要清单文件中定义。
<permission android:name="secondriver.xprovider.permission.X_PROVIDER" android:label="xProvider的读写权限" android:description="@string/permission_x_provider_desc" android:protectionLevel="normal"/> <!-- <permission android:name="secondriver.xprovider.permission.READ_X_PROVIDER" android:label="xProvider的读权限" android:description="@string/permission_read_x_provider_desc" android:protectionLevel="normal"/> <permission android:name="secondriver.xprovider.permission.WRITE_X_PROVIDER" android:label="xProvider的写权限" android:description="@string/permission_write_x_provider_desc" android:protectionLevel="normal"/> -->
权限定义时label属性的值通常为”XXX的权限“,description属性的值通常是”允许该应用干什么,授权有XXX的危害“。比如:label =发送持久广播权限 description=允许该应用发送持久广播,此类消息在广播结束后仍会保留。过多使用会占用手机过多内容,从而降低其速度或稳定性。
3.3 权限说明
默认情况下Provider是没有权限控制的,因此一旦exported=true,那么外部其它应用都可以访问到Provider提供的内容,为了更加安全,有效,范围合适的控制需要添加权限控制。
Provider权限分为:
读写的Provider层权限
读写分开的Provider层权限
Path层权限
临时授权
四种权限从上往下优先级越高。
Path层权限:是对于Uri的更具细粒度的权限控制,provider元素的子元素中可以配置grant-ui-permission和path-permission 。
临时授权:provider元素属性grantUriPermissions=true时系统将授予应用临时权限访问完整的Provider,覆盖掉Provider和Path层的权限;grantUriPermissions=false时需要在provider元素的子元素中配置一个或者多个grant-uri-permission元素来为指定的Uri的临时访问授权。
另外应用在使用临时授权访问Provider时Provider应用会在返回的Intent中通过的setFlags方法指定FLAG_GRANT_READ_URI_PERMISSION和FLAG_GRANT_WRITE_URI_PERMISSION标识,携带一个具有临时授权的Uri供外部应用完成本次内容访问操作。
4. 在本应用和其它应用中使用ContentProvider提供的内容
这里提供在其它应用中使用ContentProvider。在使用外部提供的ContentProvider通常需要了解的内容便是文字1.1部分提到。
4.1 下面是通过一个ListView来展示Note中的”CONTENT“和”CREATED“字段信息
package secondriver.oapp;
import android.app.Activity;
import android.app.LoaderManager;
import android.content.CursorLoader;
import android.content.Loader;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Author : secondriver
* Date : 2015/11/5
*/
public class XResolverActivity extends Activity {
private SimpleAdapter mAdapter;
private ListView mListView;
private List<Map<String, String>> mData;
private final int xproviderLoad = 0x01;
private LoaderManager loaderManager;
private LoaderManager.LoaderCallbacks<Cursor> callbacks = new LoaderManager.LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
CursorLoader cursorLoader = new CursorLoader(getApplicationContext(),
Uri.parse("content://secondriver.xprovider.X_PROVIDER/note"),
new String[]{
"_id",
"CONTENT",
"CREATED"
}, null, null, null);
return cursorLoader;
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
int idIndex = data.getColumnIndexOrThrow("_id");
int contentIndex = data.getColumnIndexOrThrow("CONTENT");
int createdIndex = data.getColumnIndexOrThrow("CREATED");
mData.clear();
while (data.moveToNext()) {
HashMap<String, String> m = new HashMap<>();
m.put("CONTENT", data.getString(contentIndex));
m.put("CREATED", data.getString(createdIndex));
m.put("_id", data.getString(idIndex));
mData.add(m);
}
mAdapter.notifyDataSetChanged();
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_xresolver);
mListView = (ListView) findViewById(android.R.id.list);
mData = new ArrayList<>();
mAdapter = new SimpleAdapter(this, mData, android.R.layout.simple_list_item_2,
new String[]{
"CONTENT",
"CREATED"
},
new int[]{
android.R.id.text1,
android.R.id.text2
});
mListView.setAdapter(mAdapter);
mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
HashMap m = (HashMap) mData.get(position);
String noteId = (String) m.get("_id");
String noteContent = (String) m.get("CONTENT");
getContentResolver().delete(Uri.withAppendedPath(Uri.parse("content://secondriver.xprovider.X_PROVIDER/note"), noteId), null, null);
mData.remove(position);
mAdapter.notifyDataSetChanged();
Toast.makeText(XResolverActivity.this, "Delete :" + noteContent, Toast.LENGTH_LONG).show();
}
});
loaderManager = getLoaderManager();
loaderManager.initLoader(xproviderLoad, new Bundle(), callbacks);
}
@Override
protected void onResume() {
super.onResume();
if (null != loaderManager) {
loaderManager.restartLoader(xproviderLoad, new Bundle(), callbacks);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (null != loaderManager) {
loaderManager.destroyLoader(xproviderLoad);
}
}
}
说明:需要额外注意的是代码中的硬编码字符串内容,这些内容正是XProvider类和Note类公开的信息,这些内容通常在ContentProvider组件的使用文档中公开说明的。如果是在应用内部使用XProvider的话,那么就可以直接使用变量名而避免硬编码。如下代码片段所示:
mAdapter = new SimpleAdapter(this, mData, android.R.layout.simple_list_item_2,
new String[]{
XProvider.Note.CONTENT_COLUMN,
XProvider.Note.CREATED_COLUMN
},
new int[]{
android.R.id.text1,
android.R.id.text2
});
由于XProvider的访问需要读写权限,因此需要在清单文件中声明使用的权限。
<uses-permission android:name="secondriver.xprovider.permission.X_PROVIDER"/>
5. ContentProvider组件小结
在开发ContentProvider时尽可能使其具备以下特点:
提供恰当的内容访问范围;ContentProvider组件独立封装;详细的权限,Uri,提供内容的使用说明。