。比如某个程序的数据库里存在两张表,table1 和 table2,这时就可以将路径分别命名为/table1 和/table2,然后把权限和路径进行组合,内容 URI 就变成了 com.example.app.provider/table1 和 com.example.app.provider/table2。不过,目前还很难辨认出这两个字符串就是两个内容 URI, 我们还需要在字符串的头部加上协议声明。因此,内容 URI 最标准的格式写法如下:
content://com.example.app.provider/table1 content://com.example.app.provider/table2
有没有发现,内容 URI 可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据。也正是因此,ContentResolver 中的增删改查方法才都接收 Uri 对象作为参数,因为
使用表名的话系统将无法得知我们期望访问的是哪个应用程序里的表。在得到了内容 URI 字符串之后,我们还需要将它解析成 Uri 对象才可以作为参数传入。解析的方法也相当简单
Uri uri = Uri.parse("content://com.example.app.provider/table1")
只需要调用 Uri.parse()方法,就可以将内容 URI 字符串解析成 Uri 对象了。现在我们就可以使用这个 Uri 对象来查询 table1 表中的数据了,代码如下所示:
Cursor cursor = getContentResolver().query( uri, projection, selection, selectionArgs, sortOrder);
if (cursor != null) { while (cursor.moveToNext()) { String column1 = cursor.getString(cursor.getColumnIndex("column1")); int column2 = cursor.getInt(cursor.getColumnIndex("column2")); } cursor.close(); }
ContentValues values = new ContentValues(); values.put("column1", "text"); values.put("column2", 1); getContentResolver().insert(uri, values);
可 以 看 到 , 仍 然 是 将 待 添 加 的 数 据 组 装 到 ContentValues 中 , 然 后 调 用ContentResolver 的 insert()方法,将 Uri 和 ContentValues 作为参数传入即可。现在如果我们想要更新这条新添加的数据,把 column1 的值清空,可以借助
ContentResolver 的 update()方法实现,代码如下所示:
ContentValues values = new ContentValues(); values.put("column1", ""); getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[] {"text", "1"});
注意上述代码使用了 selection 和 selectionArgs 参数来对想要更新的数据进行约束,以防止所有的行都会受影响。最后, 可以调用 ContentResolver 的 delete()方法将这条数据删除掉, 代码如下所示:
getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });
读取系统联系人
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" xmlns:android1="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" > <ListView android1:id="@+id/listView1" android1:layout_width="match_parent" android1:layout_height="wrap_content" android1:layout_alignParentLeft="true" android1:layout_alignParentTop="true" > </ListView> </RelativeLayout>
public class MainActivity extends Activity { List<String> list=new ArrayList<String>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ListView view=(ListView) findViewById(R.id.listView1); ArrayAdapter<String>adapter=new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, list); view.setAdapter(adapter); readContacts(); } private void readContacts() { // TODO Auto-generated method stub Cursor cursor=null; try { cursor=getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null); while (cursor.moveToNext()) { String displayname=cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); String number=cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)); list.add(displayname+"\n"+number); } } catch (Exception e) { // TODO: handle exception e.printStackTrace(); }finally{ if (cursor!=null) { cursor.close(); } } } }
onCreate()方法中, 我们首先获取了 ListView 控件的实例, 并给它设置好了适配器,然后就去调用 readContacts()方法。下面重点看下 readContacts()方法,可以看到,这
里使用了 ContentResolver 的 query()方法来查询系统的联系人数据。不过传入的 Uri 参数怎么有些奇怪啊,为什么没有调用 Uri.parse()方法去解析一个内容 URI 字符串呢?这是因为 ContactsContract.CommonDataKinds.Phone 类已经帮我们做好了封装, 提供了一个CONTENT_URI 常量,而这个常量就是使用 Uri.parse()方法解析出来的结果。接着我们对Cursor 对象进行遍历,将联系人姓名和手机号这些数据逐个取出,联系人姓名这一列对应的常量是 ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,联系人手机号这一列对应的常量是 ContactsContract.CommonDataKinds.Phone.NUMBER。 两个数据都取出之后,将它们进行拼接,并且中间加上换行符,然后将拼接后的数据添加到ListView 里。最后千万不要忘记将 Cursor 对象关闭掉。
<uses-permission android:name="android.permission.READ_CONTACTS" />
新建 MyProvider 继承自 ContentProvide
package com.example.owncontent; import android.content.ContentProvider; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; public class MyProvider extends ContentProvider { @Override public int delete(Uri arg0, String arg1, String[] arg2) { // TODO Auto-generated method stub return 0; } @Override public String getType(Uri arg0) { // TODO Auto-generated method stub return null; } @Override public Uri insert(Uri arg0, ContentValues arg1) { // TODO Auto-generated method stub return null; } @Override public boolean onCreate() { // TODO Auto-generated method stub return false; } @Override public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3, String arg4) { // TODO Auto-generated method stub return null; } @Override public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) { // TODO Auto-generated method stub return 0; } }
5. delete()
从内容提供器中删除数据。 使用 uri 参数来确定删除哪一张表中的数据, selection
和 selectionArgs 参数用于约束删除哪些行,被删除的行数将作为返回值返回。
6. getType()
根据传入的内容 URI 来返回相应的 MIME 类型。
可以看到,几乎每一个方法都会带有 Uri 这个参数,这个参数也正是调用
ContentResolver 的增删改查方法时传递过来的。而现在,我们需要对传入的 Uri 参数进
行解析,从中分析出调用方期望访问的表和数据。
回顾一下,一个标准的内容 URI 写法是这样的:
content://com.example.app.provider/table1
这就表示调用方期望访问的是 com.example.app 这个应用的 table1 表中的数据。除
此之外,我们还可以在这个内容 URI 的后面加上一个 id,如下所示:
content://com.example.app.provider/table1/1
这就表示调用方期望访问的是 com.example.app 这个应用的 table1 表中 id 为 1 的
数据。
内容 URI 的格式主要就只有以上两种,以路径结尾就表示期望访问该表中所有的数据,
以 id 结尾就表示期望访问该表中拥有相应 id 的数据。我们可以使用通配符的方式来分别匹
配这两种格式的内容 URI,规则如下。
1. *:表示匹配任意长度的任意字符
2. #:表示匹配任意长度的数字
所以,一个能够匹配任意表的内容 URI 格式就可以写成:
content://com.example.app.provider/*
而一个能够匹配 table1 表中任意一行数据的内容 URI 格式就可以写成:
content://com.example.app.provider/table1/#
接着,我们再借助 UriMatcher 这个类就可以轻松地实现匹配内容 URI 的功能。
UriMatcher 中提供了一个 addURI()方法,这个方法接收三个参数,可以分别把权限、路
径和一个自定义代码传进去。这样,当调用 UriMatcher 的 match()方法时,就可以将一个
Uri 对象传入,返回值是某个能够匹配这个 Uri 对象所对应的自定义代码,利用这个代码,
我们就可以判断出调用方期望访问的是哪张表中的数据了
public static final int TABLE1_DIR = 0; public static final int TABLE1_ITEM = 1; public static final int TABLE2_DIR = 2; public static final int TABLE2_ITEM = 3; private static UriMatcher uriMatcher; static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR); uriMatcher.addURI("com.example.app.provider ", "table1/#", TABLE1_ITEM); uriMatcher.addURI("com.example.app.provider ", "table2", TABLE2_ITEM); uriMatcher.addURI("com.example.app.provider ", "table2/#", TABLE2_ITEM); } ⋯⋯ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { switch (uriMatcher.match(uri)) { case TABLE1_DIR: // 查询table1表中的所有数据 break; case TABLE1_ITEM: // 查询table1表中的单条数据 break; case TABLE2_DIR: // 查询table2表中的所有数据 break; case TABLE2_ITEM: // 查询table2表中的单条数据 break; default: break; } ⋯⋯ } ⋯⋯
public class MyProvider extends ContentProvider { ⋯⋯ @Override public String getType(Uri uri) { switch (uriMatcher.match(uri)) {
case TABLE1_DIR: return "vnd.android.cursor.dir/vnd.com.example.app.provider. table1"; case TABLE1_ITEM: return "vnd.android.cursor.item/vnd.com.example.app.provider. table1"; case TABLE2_DIR: return "vnd.android.cursor.dir/vnd.com.example.app.provider. table2"; case TABLE2_ITEM: return "vnd.android.cursor.item/vnd.com.example.app.provider. table2"; default: break; } return null; }
简单起见,我们还是在上一章中 DatabaseTest 项目的基础上继续开发,通过内容提供器来给它加入外部访问接口。打开 DatabaseTest 项目,首先将 MyDatabaseHelper 中使用 Toast 弹出创建数据库成功的提示去除掉,因为跨程序访问时我们不能直接使用Toast。然后添加一个 DatabaseProvider 类,代码如下所示: public class DatabaseProvider extends ContentProvider { public static final int BOOK_DIR = 0; public static final int BOOK_ITEM = 1; public static final int CATEGORY_DIR = 2; public static final int CATEGORY_ITEM = 3; public static final String AUTHORITY = "com.example.databasetest.provider"; private static UriMatcher uriMatcher; private MyDatabaseHelper dbHelper; static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR); uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM); uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR); uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM); } @Override public boolean onCreate() { dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // 查询数据 SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = null; switch (uriMatcher.match(uri)) { case BOOK_DIR: cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder); break; case BOOK_ITEM: String bookId = uri.getPathSegments().get(1); cursor = db.query("Book", projection, "id = ?", new String[] { bookId }, null, null, sortOrder); break; case CATEGORY_DIR: 第一行代码 —— Android 296 cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder); break; case CATEGORY_ITEM: String categoryId = uri.getPathSegments().get(1); cursor = db.query("Category", projection, "id = ?", new String[] { categoryId }, null, null, sortOrder); break; default: break; } return cursor; } @Override public Uri insert(Uri uri, ContentValues values) { // 添加数据 SQLiteDatabase db = dbHelper.getWritableDatabase(); Uri uriReturn = null; switch (uriMatcher.match(uri)) { case BOOK_DIR: case BOOK_ITEM: long newBookId = db.insert("Book", null, values); uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId); break; case CATEGORY_DIR: case CATEGORY_ITEM: long newCategoryId = db.insert("Category", null, values); uriReturn = Uri.parse("content://" + AUTHORITY + "/category/" + newCategoryId); break; default: break; } return uriReturn; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // 更新数据 SQLiteDatabase db = dbHelper.getWritableDatabase(); int updatedRows = 0; switch (uriMatcher.match(uri)) { case BOOK_DIR: updatedRows = db.update("Book", values, selection, selectionArgs); break; case BOOK_ITEM: String bookId = uri.getPathSegments().get(1); updatedRows = db.update("Book", values, "id = ?", new String[] { bookId }); break; case CATEGORY_DIR: updatedRows = db.update("Category", values, selection, selectionArgs); break; case CATEGORY_ITEM: String categoryId = uri.getPathSegments().get(1); updatedRows = db.update("Category", values, "id = ?", new String[] { categoryId }); break; default: break; } return updatedRows; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // 删除数据 SQLiteDatabase db = dbHelper.getWritableDatabase(); int deletedRows = 0; switch (uriMatcher.match(uri)) { case BOOK_DIR: deletedRows = db.delete("Book", selection, selectionArgs); break; case BOOK_ITEM: String bookId = uri.getPathSegments().get(1); deletedRows = db.delete("Book", "id = ?", new String[] { bookId }); break; case CATEGORY_DIR: deletedRows = db.delete("Category", selection, selectionArgs); break; case CATEGORY_ITEM: String categoryId = uri.getPathSegments().get(1); deletedRows = db.delete("Category", "id = ?", new String[] { categoryId }); break; default: break; } return deletedRows; } @Override public String getType(Uri uri) { switch (uriMatcher.match(uri)) { case BOOK_DIR: return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.book"; case BOOK_ITEM: return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.book"; case CATEGORY_DIR: return "vnd.android.cursor.dir/vnd.com.example.databasetest. provider.category"; case CATEGORY_ITEM: return "vnd.android.cursor.item/vnd.com.example.databasetest. provider.category"; } return null; } } 代码虽然很长,不过不用担心,这些内容都非常容易理解,因为使用到的全部都是上一小节中我们学到的知识。首先在类的一开始,同样是定义了四个常量,分别用于表示访问 Book 表中的所有数据、访问 Book 表中的单条数据、访问 Category 表中的所有数据和访问 Category 表中的单条数据。 然后在静态代码块里对 UriMatcher 进行了初始化操作, 将期望匹配的几种 URI 格式添加了进去。接下来就是每个抽象方法的具体实现了,先来看下 onCreate()方法,这个方法的代码很短,就是创建了一个 MyDatabaseHelpe 的实例,然后返回 true 表示内容提供器初始化成功,这时数据库就已经完成了创建或升级操作。接着看一下 query()方法,在这个方法中先获取到了 SQLiteDatabase 的实例,然后根据传入的 Uri 参数判断出用户想要访问哪张表, 再调用 SQLiteDatabase 的 query()进行查询,并将 Cursor 对象返回就好了。注意当访问单条数据的时候有一个细节,这里调用了Uri 对象的 getPathSegments()方法,它会将内容 URI 权限之后的部分以“/”符号进行分割, 并把分割后的结果放入到一个字符串列表中, 那这个列表的第 0 个位置存放的就是路径,第 1 个位置存放的就是 id 了。得到了 id 之后,再通过 selection 和 selectionArgs 参数进行约束,就实现了查询单条数据的功能。 再往后就是 insert()方法,同样它也是先获取到了 SQLiteDatabase 的实例,然后根据传入的Uri参数判断出用户想要往哪张表里添加数据, 再调用SQLiteDatabase的insert()方法进行添加就可以了。注意 insert()方法要求返回一个能够表示这条新增数据的 URI,所以我们还需要调用 Uri.parse()方法来将一个内容 URI 解析成 Uri 对象,当然这个内容 URI是以新增数据的 id 结尾的。接下来就是 update()方法了,相信这个方法中的代码已经完全难不倒你了。也是先获 取 SQLiteDatabase 的实例,然后根据传入的 Uri 参数判断出用户想要更新哪张表里的数据,再调用 SQLiteDatabase 的 update()方法进行更新就好了,受影响的行数将作为返回值返回。下面是 delete()方法,是不是感觉越到后面越轻松了?因为你已经渐入佳境,真正地找到窍门了。这里仍然是先获取到 SQLiteDatabase 的实例,然后根据传的 Uri 参数判断出用户想要删除哪张表里的数据,再调用 SQLiteDatabase 的 delete()方法进行删除就好了,被删除的行数将作为返回值返回。 最后是 getType()方法,这个方法中的代码完全是按照上一节中介绍的格式规则编写的,相信已经没有什么解释的必要了。这样我们就将内容提供器中的代码全部编写完了, 不过离实现跨程序数据共享的功能还差了一小步,因为还需要将内容提供器在 AndroidManifest.xml 文件中注册才可以,如下所示: <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.databasetest" android:versionCode="1" android:versionName="1.0" > ⋯⋯ <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > ⋯⋯ <provider android:name="com.example.databasetest.DatabaseProvider" android:authorities="com.example.databasetest.provider" > </provider> </application> </manifest> 可以看到,这里我们使用了<provider>标签来对 DatabaseProvider 这个内容提供器进行注册,在 android:name 属性中指定了该类的全名,又在 android:authorities 属性中指定了该内容提供器的权限。现在 DatabaseTest 这个项目就已经拥有了跨程序共享数据的功能了,我们赶快来尝试一下。首先需要将 DatabaseTest 程序从模拟器中删除掉,以防止上一章中产生的遗留 数据对我们造成干扰。然后运行一下项目,将 DatabaseTest 程序重新安装在模拟器上了。接着关闭掉 DatabaseTest 这个项目,并创建一个新项目 ProviderTest,我们就将通过这个程序去访问 DatabaseTest 中的数据。还是先来编写一下布局文件吧,修改 activity_main.xml 中的代码,如下所示: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <Button android:id="@+id/add_data" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Add To Book" /> <Button android:id="@+id/query_data" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Query From Book" /> <Button android:id="@+id/update_data" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Update Book" /> <Button android:id="@+id/delete_data" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Delete From Book" /> </LinearLayout> 布局文件很简单,里面放置了四个按钮,分别用于添加、查询、修改和删除数据的。然后修改 MainActivity 中的代码,如下所示: public class MainActivity extends Activity { private String newId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button addData = (Button) findViewById(R.id.add_data); addData.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 添加数据 Uri uri = Uri.parse("content://com.example.databasetest. provider/book"); ContentValues values = new ContentValues(); values.put("name", "A Clash of Kings"); values.put("author", "George Martin"); values.put("pages", 1040); values.put("price", 22.85); Uri newUri = getContentResolver().insert(uri, values); newId = newUri.getPathSegments().get(1); } }); Button queryData = (Button) findViewById(R.id.query_data); queryData.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 查询数据 Uri uri = Uri.parse("content://com.example.databasetest. provider/book"); Cursor cursor = getContentResolver().query(uri, null, null, null, null); if (cursor != null) { while (cursor.moveToNext()) { String name = cursor.getString(cursor. getColumnIndex("name")); String author = cursor.getString(cursor. getColumnIndex("author")); int pages = cursor.getInt(cursor.getColumnIndex ("pages")); double price = cursor.getDouble(cursor. getColumnIndex("price")); Log.d("MainActivity", "book name is " + name); Log.d("MainActivity", "book author is " + author); Log.d("MainActivity", "book pages is " + pages); Log.d("MainActivity", "book price is " + price); } cursor.close(); } } }); Button updateData = (Button) findViewById(R.id.update_data); updateData.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 更新数据 Uri uri = Uri.parse("content://com.example.databasetest. provider/book/" + newId); ContentValues values = new ContentValues(); values.put("name", "A Storm of Swords"); values.put("pages", 1216); values.put("price", 24.05); getContentResolver().update(uri, values, null, null); } }); Button deleteData = (Button) findViewById(R.id.delete_data); deleteData.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // 删除数据 Uri uri = Uri.parse("content://com.example.databasetest. provider/book/" + newId); getContentResolver().delete(uri, null, null); } }); } }
可以看到,我们分别在这四个按钮的点击事件里面处理了增删改查的逻辑。添加数据的时候,首先调用了 Uri.parse()方法将一个内容 URI 解析成 Uri 对象,然后把要添加的数据都存放到 ContentValues 对象中,接着调用 ContentResolver 的 insert()方法执行添加操作就可以了。注意 insert()方法会返回一个 Uri 对象,这个对象中包含了新增数据的 id,我们通过 getPathSegments()方法将这个 id 取出,稍后会用到它。查询数据的时候,同样是调用了 Uri.parse()方法将一个内容 URI 解析成 Uri 对象,然后调用 ContentResolver 的 query()方法去查询数据, 查询的结果当然还是存放在 Cursor对象中的。之后对 Cursor 进行遍历,从中取出查询结果,并一一打印出来。更新数据的时候,也是先将内容 URI 解析成 Uri 对象,然后把想要更新的数据存放到ContentValues 对象中,再调用 ContentResolver 的 update()方法执行更新操作就可以了。注意这里我们为了不想让 Book 表中其他的行受到影响,在调用 Uri.parse()方法时,给内容 URI 的尾部增加了一个 id,而这个 id 正是添加数据时所返回的。这就表示我们只希望更新刚刚添加的那条数据,Book 表中的其他行都不会受影响。删除数据的时候,也是使用同样的方法解析了一个以 id 结尾的内容 URI,然后调用ContentResolver 的 delete()方法执行删除操作就可以了。由于我们在内容 URI 里指定了一个 id,因此只会删掉拥有相应 id 的那行数据,Book 表中的其他数据都不会受影响