在一次用Content Providers 的过程中,发现自己对它还是有很多地方很陌生,虽然google自己也说Content Providers 是android的一大特色之一,但是如果不太熟悉它的人,用起来还是有很多地方细节不明白(很明显,我就是这类人: ) )。本文主要是翻译android文档里的一篇Content Providers文章(一些比较繁琐英文描述的就被我删除了,所以如果可以还是看看英文的好http://developer.android.com/guide/topics/providers/content-providers.html),中间再加上了自己一些的理解(当然翻译有不到味的地方,见怪莫怪: ) ),最后附上网上的几个例子说明(既然网上有好的例子干嘛留着不用,吸收多点别人的设计经验嘛: ) )。目的,主要是拾取以前的遗漏的小知识点。
Content Providers 主要是用来存储和查询数据,更重要的是所有应用程序都可以访问它。这也是程序之间分享数据的唯一方法。Android里面是没有提供共同存储的区域来给所有的包访问。
Android 系统为一些常见的数据类型(如音乐、视频、图像、手机通信录联系人信息等)内置了一系列的 Content Provider, 这些都位于android.provider包下。持有特定的许可,可以在自己开发的应用程序中访问这些Content Provider。
如果自己想提供一些公共数据供大家访问,有两个方法:创建自己的content provider 或者向已有的provider添加数据——后者前提是数据是同类型并且拥有权限去写。
访问 Content Provider中的数据主要通过ContentResolver对象,ContentResolver类提供了成员方法可以用来对Content Provider 中的数据进行查询、插入、修改和删除等操作。
Content provider 实际上如何存储数据是由设计者决定。但是所有的content provider都要实现一个公共接口来查询数据和返回结果——同样也有添加,更新和删除数据。
但是这样的一个接口用户用起来不方便,大多数是在实现的acitivity里或者其他应用组件使用 getContentResolver() 来获取ContentResolver 。接着你可以使用ContentResolver的方法跟你感兴趣的content provider交互。
当一个查询来到时,android system 会在系统的某个记录里找到这个查询对应的content provider,然后运行它。(所有的ContentProvider是由系统自己来实例化)。实际上,你永远不会去直接处理ContentProvider。每个ContentProvider只有一个实例,但能够跟不同程序里的多个ContentResolver 对象交流。进程之间的交互就是由ContentProvider和ContentResolver类来处理。
Content provider 将暴露的数据作为简单的表,它基于数据库模型,这张表每行是一个记录每列是特定类型的数据。Example:
_ID |
NUMBER |
NUMBER_KEY |
LABEL |
NAME |
TYPE |
13 |
(425) 555 6677 |
425 555 6677 |
Kirkland office |
Bully Pulpit |
TYPE_WORK |
44 |
(212) 555-1234 |
212 555 1234 |
NY apartment |
Alan Vain |
TYPE_HOME |
45 |
(212) 555-6657 |
212 555 6657 |
Downtown office |
Alan Vain |
TYPE_MOBILE |
53 |
201.555.4433 |
201 555 4433 |
Love Nest |
Rex Cars |
TYPE_HOME |
每条记录都有包括一个数字 _ID 字段,他是这个表里这行记录里唯一标识。(翻译太繁琐了)简单来说就是其他表引用了这个字段,那么它可以通过这个字段来找到这个表里的具体记录。
每次查询都会返回一个Cursor 对象,它能够读取每行每列的内容。它有特殊的方法来读取每个类型的数据。所以,读取一个字段,你必须知道这个字段包含了什么数据类型。
Content URI (在开始之前先了解URI的具体部分和各个的意义)
分析URI:
A. 标准的前缀声明的数据是由content provider来控制的。它从来都不会改变
B. URI的权限部分,它是识别content provider。对于第三方应用,这个应该是一个类的全名,以保证唯一性。该权限在 <provider>
元素 authorities
的属性中声明。
<provider android:name=".TransportationProvider"
android:authorities="com.example.transportationprovider" . . . >C. 路径是content provider用来决定需要什么类型的数据。它可以是0或者更长的片段。如果content provider只展示一种数据类型(例如trains),那可以缺省。如果provider展示的是一系列类型,包括子类型——例如:"
land/bus
", "land/train
", "sea/ship
", and "sea/submarine
" 四种可能性,则后面需要添加片段(segment)。D. 如果请求不是限于一条记录,那可以省略掉后面的那个片段:
content://com.example.transportationprovider/trains
URIs
每个content provider 暴露一个公共的URI来标识唯一的数据集。Content provider 控制多个数据集并为每一个数据集提供一个URI。所有的URI开头都是一个”content://” 的字符串。这个 content: 规范标识数据是由content provider来管理的。
定义content provider 的一个好的方法是定义一个常量给URI,这简化了客户端代码和以后升级更干净。Android 自身定义了CONTENT_URI常量给provider。
URI常量通常跟content provider 起着交互作用。每个ContentResolver里的方法都将URI作为第一个参数。因为它标识着哪个provider Contentresolver需要去交流并且指明provider的哪个表。
Querying a content provider
你需要三种信息来查询一个content provider:
1. 标识provider的URI
2. 你希望接收的字段名
3. 这些字段的数据类型
当然如果你查询的是某个字段,那也需要这个记录的ID
Making the query
查询content provider,可以使用ContentResolver.query()或者Acitivity.managedQuery()。两个方法都需要同样的参数,返回同样的Cursor对象。(managedQuery() and getContentREsolver().query()的区别:使用managedQuery()后返回cursor,activity会保持一个引用到cursor,无论什么时候都可以关闭,例如当调用onDestroy时,activity会自动调用,无需自己手动去调用close。但是如果使用query(),那就要自己去管理Cursor,如果在onDestory时忘记去close,将会泄露内存,当然可以使用 Activity.startManagingCursor()
交给
activity
管理
)。如何给URI加入查询条件,如有ID为23,URI将会这样:
content://. . . ./23
可以使用 ContentUris.withAppendedId() and Uri.withAppendedPath(),它们使加入查询条件变简单了。Example:(两种示例,第一个是以int形式加入,第二个是以字符形式加入)
import android.provider.Contacts.People; import android.content.ContentUris; import android.net.Uri; import android.database.Cursor; // Use the ContentUris method to produce the base URI for the contact with _ID == 23. Uri myPerson = ContentUris.withAppendedId(People.CONTENT_URI, 23); // Alternatively, use the Uri method to produce the base URI. // It takes a string rather than an integer. Uri myPerson = Uri.withAppendedPath(People.CONTENT_URI, "23"); // Then query for this specific record: Cursor cur = managedQuery(myPerson, null, null, null, null);query() and managedQuery() 方法的参数讲解(除第一个URI参数):(结合下面例子来说)
import android.provider.Contacts.People; import android.database.Cursor; // Form an array specifying which columns to return. String[] projection = new String[] { People._ID, People._COUNT, People.NAME, People.NUMBER }; // Get the base URI for the People table in the Contacts content provider. Uri contacts = People.CONTENT_URI; // Make the query. Cursor managedCursor = managedQuery(contacts, projection, // Which columns to return null, // Which rows to return (all rows) null, // Selection arguments (none) // Put the results in ascending order by name People.NAME + " ASC");
1. 第二个参数,projection变量,返回的结果是该传入变量里所列举的字段名。如果该参数null则返回所有字段数据。
2. 第三个参数,where 后的参数(不包括where和数值),返回符合这些参数的值的数据,要跟第四个参数结合使用。
3. 第四个参数,where 后参数的值。
4. 排序(不包括order by)
上面的例子主要是返回联系人的数据。
What a query returns
接上面的例子,一个查询返回的是一系列的zero或者数据库记录。
_ID |
_COUNT |
NAME |
NUMBER |
44 |
3 |
Alan Vain |
212 555 1234 |
13 |
3 |
Bully Pulpit |
425 555 6677 |
53 |
3 |
Rex Cars |
201 555 4433 |
接收到的数据是通过Cursor对象来获取,它通常可以向前和向后遍历整个结果集。但是这只能读,如果想添加、更新和删除就必须使用ContentResolver对象。
你可以通过特定字段读取记录里的数据,前提是必须知道这个字段的数据类型,因为Cursor对象分开不同的方法去读取每种数据类型——例如 getString(), getInt(), and getFloat(). Cursor允许你通过该列的位置来获取某个列的名称,又或者通过该列名称或者该列的位置。
下面这个示例片段是读取Cursor里面的数据
import android.provider.Contacts.People; private void getColumnData(Cursor cur){ if (cur.moveToFirst()) { String name; String phoneNumber; int nameColumn = cur.getColumnIndex(People.NAME); int phoneColumn = cur.getColumnIndex(People.NUMBER); String imagePath; do { // Get the field values name = cur.getString(nameColumn); phoneNumber = cur.getString(phoneColumn); // Do something with the values. ... } while (cur.moveToNext()); } }
如果查询返回二进制数据,如图片或者声音,这个数据可能直接保存在表里或者保存的是URI,然后可通过它去拿到数据。少量数据(20-50k或者更少)通常是直接保存在表里并且可以通过getBlob()来获取,返回byte数组。
但是如果表里是URI,你永远不能直接去打开读取(因为权限的问题会导致失败)。相反,你应该调用ContentResolver.openInputStream() 获取InputStream对象来读取数据(后面有详细说明如果读)。
数据可以通过以下方式改变
1. 添加一个新的记录
2. 添加新值到存在的记录
3. 批量更新存在的记录
4. 删除记录
所有数据的修改都是通过使用ContentResolver方法来完成。一些content provider写需要比读更严格的权限。如果没有权限去写content provider,ContentResolver方法将调用失败。
向content provider添加一个新的记录,首先创建ContentValues对象,每个key对应列的名称,value就是所要插入到列的值。调用 ContentResolver.insert() 并传递这个provider所需要的URI和 ContentValues。结果将返回一个新的记录 ,它以URI形式返回——你可以使用它去查询获取新的记录然后修改记录。Example:
import android.provider.Contacts.People; import android.content.ContentResolver; import android.content.ContentValues; ContentValues values = new ContentValues(); // Add Abraham Lincoln to contacts and make him a favorite. values.put(People.NAME, "Abraham Lincoln"); // 1 = the new contact is added to favorites // 0 = the new contact is not added to favorites values.put(People.STARRED, 1); Uri uri = getContentResolver().insert(People.CONTENT_URI, values);
一旦记录存在,你可以添加新的信息或者修改存在的信息。例如,下一步在上例中将添加联系人信息--像电话号码或IM或e-mail地--给新的条目。 添加记录到联系人数据最好的方法是在URI后面加上要添加数据到该表的表名。然后使用修改过的URI添加数据。
Uri phoneUri = null; Uri emailUri = null; // Add a phone number for Abraham Lincoln. Begin with the URI for // the new record just returned by insert(); it ends with the _ID // of the new record, so we don't have to add the ID ourselves. // Then append the designation for the phone table to this URI, // and use the resulting URI to insert the phone number. phoneUri = Uri.withAppendedPath(uri, People.Phones.CONTENT_DIRECTORY); values.clear(); values.put(People.Phones.TYPE, People.Phones.TYPE_MOBILE); values.put(People.Phones.NUMBER, "1233214567"); getContentResolver().insert(phoneUri, values); // Now add an email address in the same way. emailUri = Uri.withAppendedPath(uri, People.ContactMethods.CONTENT_DIRECTORY); values.clear(); // ContactMethods.KIND is used to distinguish different kinds of // contact methods, such as email, IM, etc. values.put(People.ContactMethods.KIND, Contacts.KIND_EMAIL); values.put(People.ContactMethods.DATA, "[email protected]"); values.put(People.ContactMethods.TYPE, People.ContactMethods.TYPE_HOME); getContentResolver().insert(emailUri, values);
你可以通过调用ContentValues.put()放置小量的二进制数据到表中。这对小icon的图片或者短的音频有效。然后如果你要放置大量的二进制数据,如图像或者完整的歌曲,可使用URI来代替数据并且调用 ContentResolver.openOutputStream() 传入文件的URI。
就这一点而言,MediaStore content provider,它主要提供分配图片,音频和视频数据,利用特定的协议:可以通过使用URI来获取MediaStore里的二进制数据,如使用openInputStream()
来获取数据。同样,使用openOutputStream()
放置二进制数据到MediaStore。
import android.provider.MediaStore.Images.Media; import android.content.ContentValues; import java.io.OutputStream; // Save the name and description of an image in a ContentValues map. ContentValues values = new ContentValues(3); values.put(Media.DISPLAY_NAME, "road_trip_1"); values.put(Media.DESCRIPTION, "Day 1, trip to Los Angeles"); values.put(Media.MIME_TYPE, "image/jpeg"); // Add a new record without the bitmap, but with the values just set. // insert() returns the URI of the new record. Uri uri = getContentResolver().insert(Media.EXTERNAL_CONTENT_URI, values); // Now get a handle to the file for that record, and save the data into it. // Here, sourceBitmap is a Bitmap object representing the file to save to the database. try { OutputStream outStream = getContentResolver().openOutputStream(uri); sourceBitmap.compress(Bitmap.CompressFormat.JPEG, 50, outStream); outStream.close(); } catch (Exception e) { Log.e(TAG, "exception while writing image", e); }
批量更新一组记录(如在所有的字段里将“NY”改成“new york”),调用 ContentResolver.update() 方法传入指定列和值。
Deleting a record
删除单条记录,调用 ContentResolver.delete()传入指定行的URI。
删除多条记录,调用 ContentResolver.delete()传入要删除记录的类型的URI (例如: android.provider.Contacts.People.CONTENT_URI
加上 SQL的 WHERE
来定义要删除的列)
创建content provider步骤:
1. 建立存储数据的程序系统。大多数content provider存储数据使用android的文件存储仓库或者数据库,但你可以用你所想要的方法来存储数据。Android 提供SQLiteOpenHelper类来帮助你创建数据库和SQLiteDatabase和管理它。
2. 继承扩展ContentProvider 类来提供访问数据的权限。
3. 在manifest文件中给你程序声明content provider
继承ContentProvider后,主要需要实现6个抽象方法:
query()
insert()
update()
delete()
getType()
onCreate()
query() 方法返回Cursor对象,它可以遍历需要的数据。Cursor本身是个接口,但是android 提供一些做好的Cursor对象供你使用。例如,SQLiteCursor能遍历存储在数据库里的数据。通过调用SQLiteDatabase类的query()方法获取Cursor。这里也有其他实现Cursor——如MatrixCursor——不过数据不是放到数据库里
因为这些ContentProvider方法能被不同的ContentResolver对象在不同进程和线程里调用,因此实现它必须以线程安全的方式。
最后,当修改数据完后你应该调用 ContentResolver.notifyChange() 来通知监听者。
在继承该类之后,你应该用以下的方式使自己的类更通俗易懂:
1. 定义 public static final URI 名称叫CONTENT_URI。你必须定义唯一的字符串作为值。最好的解决方案是使用该类的全面(都是小写)。Example:
public static final Uri CONTENT_URI = Uri.parse("content://com.example.codelab.transportationprovider");
如果provider有子表,也需要定义CONTENT_URI常量给每个子表。这些URI有相同的权限,不同是在他们的路径部分。如:
content://com.example.codelab.transportationprovider/train content://com.example.codelab.transportationprovider/air/domestic content://com.example.codelab.transportationprovider/air/international
2. 定义content provider返回给客户端的列的名称。
确定记录里一定要包括int类型名叫“_id”的列,不管你是否已经定义了其他的字段,但是这个字段一定要有,因为它是作为所有记录里的唯一标识。如果你使用SQLite 数据库,_ID 字段应该是下面的格式:
INTEGER PRIMARY KEY AUTOINCREMENT
3. 仔细注释每一列的数据类型,用户需要这些信息来读懂数据的意思。
4. 如果你正处理一个新的数据类型。你必须在实现ContentProvider.getType()里定义一个新的MIME类型然后返回。 (后面这段看起来挺抽象的,所以我就直接以原文放上)The type depends in part on whether or not the content:
URI submitted to getType()
limits the request to a specific record。MIME类型有两种方式,一种是单条记录,一种是多条记录。Example:
单个记录:
vnd.android.cursor.item/vnd.yourcompanyname.contenttype //For example, a request for train record 122, like this URI, content://com.example.transportationprovider/trains/122 //might return this MIME type: //vnd.android.cursor.item/vnd.example.rail 多个记录: vnd.android.cursor.dir/vnd.yourcompanyname.contenttype //For example, a request for all train records, like the following URI, content://com.example.transportationprovider/trains //might return this MIME type: //vnd.android.cursor.dir/vnd.example.rail
5. 如果你展示的byte数据太大而不能放到表里——如大的图片文件——那这个字段应该以URI字符形式展示。这个字段允许用户通过它访问文件。这个记录应该有另一个字段名叫“_data”,它列举了这个文件在设备上的精确路径。但是这个字段不能在客户端读取,只能在ContentResolver中获取。(如何读取前面已经有说明)
在开发中为了系统能了解你的content provider,你应该在 AndroidManifest.xml file中声明 <provider>
标签。如果没有声明,则系统是不知道你定义了content provider。
Name 属性对应着contentprovider子类的全名,authorities 属性是URI部分中的权限。例如如果contentprovider的子类是AutoInfoProvider,下面<provider>
元素将看起来如下:
<provider android:name="com.example.autos.AutoInfoProvider"
android:authorities="com.example.autos.autoinfoprovider"
. . . /></provider>从以上可看出authorities属性是URI部分的之一。例如:
content://com.example.autos.autoinfoprovider/honda
content://com.example.autos.autoinfoprovider/gm/compact
content://com.example.autos.autoinfoprovider/gm/suv
这些路径不会在manifest中声明。authority 只是识别provider,不是路径。因此你的provider是可以分析出任何URI里的部分路径是否具有该权限。
Other的
<provider>
属性标签具体可以查看相关文档。
///////////////下面是示例代码,当然是引用别人的,下面有链接
package jason.wei.apps.securenotes.providers; import jason.wei.apps.securenotes.db.Note.Notes; import java.util.HashMap; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.util.Log; /** * @author Jason Wei * */ public class NotesContentProvider extends ContentProvider { private static final String TAG = "NotesContentProvider"; private static final String DATABASE_NAME = "notes.db"; private static final int DATABASE_VERSION = 1; private static final String NOTES_TABLE_NAME = "notes"; public static final String AUTHORITY = "jason.wei.apps.notes.providers.NotesContentProvider"; private static final UriMatcher sUriMatcher; private static final int NOTES = 1; private static HashMap<String, String> notesProjectionMap; 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 TABLE " + NOTES_TABLE_NAME + " (" + Notes.NOTE_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + Notes.TITLE + " VARCHAR(255)," + Notes.TEXT + " LONGTEXT" + ");"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion + ", which will destroy all old data"); db.execSQL("DROP TABLE IF EXISTS " + NOTES_TABLE_NAME); onCreate(db); } } private DatabaseHelper dbHelper; @Override public int delete(Uri uri, String where, String[] whereArgs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); int count; switch (sUriMatcher.match(uri)) { case NOTES: count = db.delete(NOTES_TABLE_NAME, where, whereArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } @Override public String getType(Uri uri) { switch (sUriMatcher.match(uri)) { case NOTES: return Notes.CONTENT_TYPE; default: throw new IllegalArgumentException("Unknown URI " + uri); } } @Override public Uri insert(Uri uri, ContentValues initialValues) { if (sUriMatcher.match(uri) != NOTES) { throw new IllegalArgumentException("Unknown URI " + uri); } ContentValues values; if (initialValues != null) { values = new ContentValues(initialValues); } else { values = new ContentValues(); } SQLiteDatabase db = dbHelper.getWritableDatabase(); long rowId = db.insert(NOTES_TABLE_NAME, Notes.TEXT, values); if (rowId > 0) { Uri noteUri = ContentUris.withAppendedId(Notes.CONTENT_URI, rowId); getContext().getContentResolver().notifyChange(noteUri, null); return noteUri; } throw new SQLException("Failed to insert row into " + uri); } @Override public boolean onCreate() { dbHelper = new DatabaseHelper(getContext()); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); switch (sUriMatcher.match(uri)) { case NOTES: qb.setTables(NOTES_TABLE_NAME); qb.setProjectionMap(notesProjectionMap); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); c.setNotificationUri(getContext().getContentResolver(), uri); return c; } @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { SQLiteDatabase db = dbHelper.getWritableDatabase(); int count; switch (sUriMatcher.match(uri)) { case NOTES: count = db.update(NOTES_TABLE_NAME, values, where, whereArgs); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri, null); return count; } static { sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); sUriMatcher.addURI(AUTHORITY, NOTES_TABLE_NAME, NOTES); notesProjectionMap = new HashMap<String, String>(); notesProjectionMap.put(Notes.NOTE_ID, Notes.NOTE_ID); notesProjectionMap.put(Notes.TITLE, Notes.TITLE); notesProjectionMap.put(Notes.TEXT, Notes.TEXT); } } public class Note { public Note() { } public static final class Notes implements BaseColumns { private Notes() { } public static final Uri CONTENT_URI = Uri.parse("content://" + NotesContentProvider.AUTHORITY + "/notes"); public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.jwei512.notes"; public static final String NOTE_ID = "_id"; public static final String TITLE = "title"; public static final String TEXT = "text"; } }
引用该代码的链接:http://www.haoni.org/2011/03/09/androidcontentproviderhecontentresolver/
getType方法的解析
http://blog.csdn.net/tracy4u/archive/2011/02/14/6183971.aspx