[Android学习笔记一] ContentProvider组件开发详解
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组件代码框架 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 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 StringTAG= "XProvider" ; //授权字符串 //授权Uri //Note公开信息 public static final class Note implements BaseColumns{ //Note表名 //Note表的内容Uri //内容类型 //Note表字段 //Uri匹配码 } //Uri匹配器 public static UriMatcheruriMatcher; static { uriMatcher= new UriMatcher(UriMatcher.NO_MATCH); //添加Uri匹配内容 } private XSQLiteHelperhelper; @Override public boolean onCreate(){ //ContentProvider组件创建时做的工作 return true ; } @Override public Cursorquery(Uriuri,String[]projection,Stringselection,String[]selectionArgs,StringsortOrder){ return cursor; } @Override public StringgetType(Uriuri){ return null ; } @Override public Uriinsert(Uriuri,ContentValuesvalues){ return null ; } @Override public int delete(Uriuri,Stringselection,String[]selectionArgs){ return effectRows; } @Override public int update(Uriuri,ContentValuesvalues,Stringselection,String[]selectionArgs){ return effectRows; } public static class XSQLiteHelper extends SQLiteOpenHelper{ public static final StringDATA_BASE_NAME= "xprovider.sqlite" ; public static final int DATA_BASE_VERSION= 1 ; public static volatile XSQLiteHelperxsqLiteHelper; public static XSQLiteHelpergetXsqLiteHelper(Contextcontext){ //实例化XSQLiteHelp对象 return xsqLiteHelper; } public XSQLiteHelper(Contextcontext,Stringname,SQLiteDatabase.CursorFactoryfactory, int version){ super (context,name,factory,version); } @Override public void onCreate(SQLiteDatabasedb){ //数据库初始化 } @Override public void onUpgrade(SQLiteDatabasedb, int oldVersion, int newVersion){ } } } 上面代码提供了一个SQLiteOpenHelper的实现,实际开发中可个会更具具体应用来提供给ContentProvider组件。 中文注释部分可能需要填充具体代码,比如权限可以更具实际情况来定义,表字段根据要提供的内容信息来公开字段名并且需要在文档中说明。 2.1 准备Note表信息 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 //Note公开信息 public static final class Note implements BaseColumns{ //Note表名 public static final StringTABLE_NAME= "note" ; //Note表的内容Uri public static final UriCONTENT_URI=Uri.withAppendedPath(AUTHORITY_URI, "note" ); //建议使用格式如:type/subtype=>vnd.android.cursor.dir/vnd.<name>.<type>name=packagenametype=tablename public static final StringCONTENT_DIR_TYPE= "vnd.android.cursor.dir/vnd.secondriver.xprovider.note" ; public static final StringCONTENT_ITEM_TYPE= "vnd.android.cursor.item/vnd.secondriver.xprovider.note" ; //ID主键 public static final StringID_COLUMN=_ID; //内容列 public static final StringCONTENT_COLUMN= "CONTENT" ; //创建时间列表 public static final StringCREATED_COLUMN= "CREATED" ; //标识列 public static final StringFLAG_COLUMN= "FLAG" ; //状态列 public static final StringSTATUS_COLUMN= "STATUS" ; //Uri匹配码 private static final int NOTE_ITEM= 0x21 ; private static final int NOTE_DIR= 0x22 ; } 说明:Uri匹配码声明为Note类的常量这样可以方便对照,一个ContentProvider组件中声明多个表公开类,比如User类,这样就比较容易区分操作的是那个表的内容。 2.2 Note类外其它的具体代码 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 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 StringTAG= "XProvider" ; //授权字符串 public static final StringAUTHORITY= "secondriver.xprovider.X_PROVIDER" ; //授权Uri public static final UriAUTHORITY_URI=Uri.parse( "content://" +AUTHORITY); //Uri匹配器 public static UriMatcheruriMatcher; static { uriMatcher= new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(AUTHORITY, "note/#" ,Note.NOTE_ITEM); uriMatcher.addURI(AUTHORITY, "note" ,Note.NOTE_DIR); } private XSQLiteHelperhelper; @Override public boolean onCreate(){ helper=XSQLiteHelper.getXsqLiteHelper(getContext()); return true ; } @Override public Cursorquery(Uriuri,String[]projection,Stringselection,String[]selectionArgs,StringsortOrder){ SQLiteQueryBuilderbuilder= 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( "UnsupportedURI:" +uri); } builder.setTables(Note.TABLE_NAME); Cursorcursor=builder.query(helper.getReadableDatabase(),projection,selection,selectionArgs, null , null ,sortOrder); return cursor; } @Override public StringgetType(Uriuri){ 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( "UnsupportedURI:" +uri); } } @Override public Uriinsert(Uriuri,ContentValuesvalues){ SQLiteDatabasedb=helper.getWritableDatabase(); switch (uriMatcher.match(uri)){ case Note.NOTE_DIR: long noteId=db.insert(Note.TABLE_NAME, null ,values); if (noteId> 0 ){ //插入成功 UrinewUri=ContentUris.withAppendedId(uri,noteId); getContext().getContentResolver().notifyChange(newUri, null ); return newUri; } else { //插入失败 return null ; } default : throw new IllegalArgumentException( "UnsupportedURI:" +uri); } } @Override public int delete(Uriuri,Stringselection,String[]selectionArgs){ SQLiteDatabasedb=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); StringwhereClause=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( "UnsupportedURI:" +uri); } getContext().getContentResolver().notifyChange(uri, null ); return effectRows; } @Override public int update(Uriuri,ContentValuesvalues,Stringselection,String[]selectionArgs){ SQLiteDatabasedb=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); StringwhereClause=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( "UnsupportedURI:" +uri); } return effectRows; } public static class XSQLiteHelper extends SQLiteOpenHelper{ public static final StringDATA_BASE_NAME= "xprovider.sqlite" ; public static final int DATA_BASE_VERSION= 1 ; public static volatile XSQLiteHelperxsqLiteHelper; public static XSQLiteHelpergetXsqLiteHelper(Contextcontext){ if ( null ==xsqLiteHelper){ synchronized (XSQLiteHelper. class ){ if ( null ==xsqLiteHelper){ xsqLiteHelper= new XSQLiteHelper(context,DATA_BASE_NAME, null ,DATA_BASE_VERSION); } } } return xsqLiteHelper; } public XSQLiteHelper(Contextcontext,Stringname,SQLiteDatabase.CursorFactoryfactory, int version){ super (context,name,factory,version); } @Override public void onCreate(SQLiteDatabasedb){ db.beginTransaction(); try { db.execSQL( new StringBuilder( "createtableifnotexists" ) .append(Note.TABLE_NAME) .append( "(" ) .append(Note.ID_COLUMN) .append( "integerprimarykeyautoincrement," ) .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() ); SimpleDateFormatsimpleDateFormat= new SimpleDateFormat( "yyyy-MM-ddHH:mm:ss" ); for ( int i= 0 ;i< 50 ;i++){ ContentValuescv= 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 (SQLiteExceptione){ Log.e(TAG,e.getMessage()); } finally { db.endTransaction(); } } @Override public void onUpgrade(SQLiteDatabasedb, int oldVersion, int newVersion){ } } } 3. 在AndroidMainfest.xml清单文件中声明ContentProvider组件 3.1 声明XProvider 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <!-- 按如下顺序验证权限 android:grantUriPermssions:Temporarypermissionflag. android:permission:Singleprovider-wideread/writepermission. android:readPermission:Provider-widereadpermission. android:writePermission:Provider-widewritepermission. --> < 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” 因此该权限需要清单文件中定义。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 < 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“字段信息 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 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 SimpleAdaptermAdapter; private ListViewmListView; private List<Map<String,String>>mData; private final int xproviderLoad= 0x01 ; private LoaderManagerloaderManager; private LoaderManager.LoaderCallbacks<Cursor>callbacks= new LoaderManager.LoaderCallbacks<Cursor>(){ @Override public Loader<Cursor>onCreateLoader( int id,Bundleargs){ CursorLoadercursorLoader= 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,Cursordata){ 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(BundlesavedInstanceState){ 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,Viewview, int position, long id){ HashMapm=(HashMap)mData.get(position); StringnoteId=(String)m.get( "_id" ); StringnoteContent=(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的话,那么就可以直接使用变量名而避免硬编码。如下代码片段所示: 1 2 3 4 5 6 7 8 9 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的访问需要读写权限,因此需要在清单文件中声明使用的权限。 1 < uses-permission android:name = "secondriver.xprovider.permission.X_PROVIDER" /> 5. ContentProvider组件小结 在开发ContentProvider时尽可能使其具备以下特点: 提供恰当的内容访问范围;ContentProvider组件独立封装;详细的权限,Uri,提供内容的使用说明。 本文转自 secondriver 51CTO博客,原文链接:http://blog.51cto.com/aiilive/1710151,如需转载请自行联系原作者