内容提供器(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,它提供一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问程序中的数据安全性。它的用法有两种:1.使用现有的内容提供器来读取或操作相应程序的数据。2.自己创建一个内容提供器给我们程序的数据提供外部访问接口。
要想访问内容提供器中共享的数据,就一定要借助ContentResolver类,具体是通过Context的getContentResolver()方法来拿到ContentResolver实例。而利用这个实例的query()、update()、insert()、delete()方法就可以实现对数据的操作。显然这些方法都需要接受一个参数,那就是URI。
这个参数URI被称为内容URI,它由三部分组成:协议声明,内容URI的协议声明写上"content://"即可;权限,通常采用程序包名进行命名,如接下来将举一个例子包名为company.contactstest,权限则命名为company.contactstest.provider;路径,用于对同一程序中的不同表做区分,如/Book,/Category。
上面拿到的URI还不是Uri对象,需要通过解析才能得到对象。Uri uri = Uri.parse("content://company.contactstest.provider/Book")。(括号中即为一个标准的内容URI写法)
关于具体的增删改查方法与SQLiteDatabase类似,我们就在例子中来介绍吧。
下面通过读取手机通讯录中的联系人来介绍如何使用系统现有的内容提供器。事先在通讯录中随便增加了两个联系人。
然后新建一个ContactsTest项目,并在主活动的布局中简单的放入一个ListView控件,代码略。
package company.contactstest; import android.database.Cursor; import android.provider.ContactsContract; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.ArrayAdapter; import android.widget.ListView; import java.util.ArrayList; import java.util.List; public class MainActivity extends AppCompatActivity { ListView contactListView; ArrayAdapteradapter; List contactsList = new ArrayList (); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); contactListView = (ListView) findViewById(R.id.contacts_list_view); adapter = new ArrayAdapter (this, android.R.layout.simple_list_item_1, contactsList); contactListView.setAdapter(adapter); readContacts(); } private void readContacts() { 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)); contactsList.add(displayName + "\n" + number); } }catch (Exception e) { e.printStackTrace(); } finally { if (cursor != null) { cursor.close(); } } } }
可以看到,首先准备了ListView的相关代码,都是ListView的基本用法不作过多解释。然后将重点放在我们创建的readContacts方法上。getContentResolver()得到实例,然后调用query()方法,其中传入的第一个参数就是URI,只不过这里是一个系统封装后提供的CONTENT_URI常量,因此不需要再用Uri.parse()方法解析,后面四个参数都传入null就表示我们要查询该表中的所有行且结果无需排序。最后将查询到的结果返回给一个Cursor对象,并且将其中两个数据DISPLAY_NAME和NUMBER都取出来传给contactsList即可显示在ListView上了。
接下来创建一个自己的内容提供器并为我之前一篇SQL的博客建立的项目提供外部接口,这里先打开之前的项目,在里面新建一个DatabaseProvider类并让其继承自ContentProvider,这个父类中有六个抽象方法:onCreate()、qeury()、insert()、update()、delete()、getType(),我们在使用子类继承它的时候需要将这六个方法全部重写。
package geekband.testsql; import android.content.ContentProvider; import android.content.ContentValues; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.support.annotation.Nullable; /** * Created by samyang on 2016/8/31. */ 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 = "geekband.testsql.provider"; private static UriMatcher mUriMatcher; private MyDatabaseHelper dbHelper; static { mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); mUriMatcher.addURI(AUTHORITY, "book", BOOK_DIR); mUriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM); mUriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR); mUriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM); } @Override public boolean onCreate() { dbHelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 4); return true; } @Nullable @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { //查询数据 SQLiteDatabase db = dbHelper.getReadableDatabase(); Cursor cursor = null; switch (mUriMatcher.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: 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; } @Nullable @Override public String getType(Uri uri) { switch (mUriMatcher.match(uri)) { case BOOK_DIR: return "vnd.android.cursor.dir/vnd.geekband.testsql.provider.book"; case BOOK_ITEM: return "vnd.android.cursor.item/vnd.geekband.testsql.provider.book"; case CATEGORY_DIR: return "vnd.android.cursor.dir/vnd.geekband.testsql.provider.category"; case CATEGORY_ITEM: return "vnd.android.cursor.item/vnd.geekband.testsql.provider.category"; } return null; } @Nullable @Override public Uri insert(Uri uri, ContentValues values) { //增加数据 SQLiteDatabase db = dbHelper.getWritableDatabase(); Uri uriReturn = null; switch (mUriMatcher.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 delete(Uri uri, String selection, String[] selectionArgs) { //删除数据 SQLiteDatabase db = dbHelper.getWritableDatabase(); int deleteRows = 0; switch (mUriMatcher.match(uri)) { case BOOK_DIR: deleteRows = db.delete("Book", selection, selectionArgs); break; case BOOK_ITEM: String bookId = uri.getPathSegments().get(1); deleteRows = db.delete("Book", "id = ?", new String[] {bookId}); break; case CATEGORY_DIR: deleteRows = db.delete("Book", selection, selectionArgs); break; case CATEGORY_ITEM: String categoryId = uri.getPathSegments().get(1); deleteRows = db.delete("Book", "id = ?", new String[] {categoryId}); break; default: break; } return deleteRows; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { //更新数据 SQLiteDatabase db = dbHelper.getWritableDatabase(); int updateRows = 0; switch (mUriMatcher.match(uri)) { case BOOK_DIR: updateRows = db.update("Book", values, selection, selectionArgs); break; case BOOK_ITEM: String bookId = uri.getPathSegments().get(1); updateRows = db.update("Book", values, "id = ?", new String[] {bookId}); break; case CATEGORY_DIR: updateRows = db.update("Category", values, selection, selectionArgs); break; case CATEGORY_ITEM: String categoryId = uri.getPathSegments().get(1); updateRows = db.update("Category", values, "id = ?", new String[] {categoryId}); break; default: break; } return updateRows; } }
然后你会发现除了刚才提到的六个抽象方法外还多了很多别的代码,下面逐一来分析它:
首先回顾一下URI的标准写法,在这里应该写为:content://geekband.testsql.provider/book。除此之外,我们还可以在这个内容URI后面加上ID,就可以表示访问这个表中拥有这个ID的数据了。所以一个能匹配任意表的内容URI格式就是:content://geekband.testsql.provider/*,其中“*”表示匹配任意长度的任意字符;一个能匹配book表中任意一行数据的内容URI格式就是:content://geekband.testsql.provider/book/#,其中“#”表示匹配任意长度的数字。
接着,我们再借助UriMatcher()这个类就可以轻松地实现匹配内容URI的功能。这个类提供了一个addURI()方法,其中传入三个参数:权限、路径、一个自定义的代码。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了。于是我在重写抽象方法之前先定义了4个常量,然后在静态块中创建了mUriMatcher这个实例,并调用了addURI方法传入参数来将自定义代码和调用方期望访问的数据联系起来了。
然后就是getType()方法,它是所有内容提供器都必须提供的一个方法。用于根据传入的内容URI来返回相应的MIME类型。至于内容URI所对应的MIME字符串Android做了如下格式规定:1.必须以vnd开头。2.如果内容URI以路径结尾,则后接android.cursor.dir/,如果内容URI以id结尾,则后接android.cursor.item/。3.最后接上vnd.
再接着是onCreate()方法,因为我们需要在这里完成数据库的创建或者升级的操作,当然还是要借助数据库的帮助类是实现。但这里不同之处在于这里的onCreate()方法会返回一个布尔型值表示是否创建成功。因为只有当ContentResolver尝试访问我们程序中的数据时ContentProvider才会被初始化。
最后是qeury()、insert()、update()、delete()四个方法,他们传入的第一个参数都是uri,用来指定要操作的表。以及注意一下各方法的返回值:query()方法查询的结果都保存在Cursor对象中因此返回的是一个Cursor对象、insert()方法返回的是增加的那条信息的Uri、update()和delete()的操作都是针对某一行因此返回的是程序操作那一行的Int型变量。其他参数都跟SQL中的用法一样,不再赘述。另外注意当访问单条数据的时候有一个细节,这里调用了Uri对象getPathSegment()方法,它会将内容URI权限之后的部分以"/"符号进行分割,并把分割后的结果放入到一个字符串列表中,那这个列表的第0个位置存放的就是路径,第1个位置存放的就是ID了,得到ID之后才可以进行相关的约束。
最后的最后,当然provider是需要注册的。
android:authorities="geekband.testsql.provider"
android:name=".DatabaseProvider"
android:exported="true">
这样我们在TestSQL项目中的内容提供器就构建好了,接下来我们要新建一个ProviderTest项目、看一下新建的这个项目是否能通过刚才建立的ContentProvider对TestSQL项目进行数据访问。
布局很简单,仍然是放上几个按钮,指定ID。代码略。
package company.providertest; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.view.View; import android.widget.Button; public class MainActivity extends AppCompatActivity { 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 View.OnClickListener() { @Override public void onClick(View view) { //添加数据 Uri uri = Uri.parse("content://geekband.testsql.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 View.OnClickListener() { @Override public void onClick(View view) { //查找数据 Uri uri = Uri.parse("content://geekband.testsql.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 page is " + pages); Log.d("MainActivity", "book price is " + price); } cursor.close(); } } }); Button updateData = (Button) findViewById(R.id.update_data); updateData.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //更新数据 Uri uri = Uri.parse("content://geekband.testsql.provider/book/" + newId); ContentValues values = new ContentValues(); values.put("name", "The Lord of the Rings"); values.put("author", "John Ronald Reuel Tolkien"); values.put("pages", 3699); values.put("price", 129.00); getContentResolver().update(uri, values, null, null); } }); Button deleteData = (Button) findViewById(R.id.delete_data); deleteData.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { //删除数据 Uri uri = Uri.parse("content://geekband.testsql.provider/book/" + newId); getContentResolver().delete(uri, null, null); } }); } }
拿到四个按钮后,分别在里面添加了点击事件,处理了增删改查的逻辑。
添加数据的时候,首先调用了Uri.parse()方法将一个内容URI解析成Uri对象,然后把要添加的数据存放到ContentValues对象中,接着调用ContentResolver的insert()方法执行添加操作就可以了。在insert()方法中,我们返回了一个Uri对象,然后通过getPathSegment()方法得到了新增这条数据的id,这是给下面的delete()、update()方法操作这条数据而准备的。
其他操作跟添加数据操作一样,都要先调用了Uri.parse()方法将一个内容URI解析成Uri对象。除此之外,查询数据仍然要借助Cursor对象,更新数据也是用的ContentValues,删除数据直接删掉uri即可。当然我们还在query()中打了log以便观察。
点击ADD TO BOOK 然后点击QUERY FROM BOOK可以看到如下图所示Log:
然后点击UPDATE BOOK再点击QUERY FROM BOOK后如下:
可以看出,我们已经成功实现了通过给一个程序构建内容提供器并提供接口从而让外部程序访问该程序数据内容的功能。