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,提供内容的使用说明。