当数据需要在应用程序间共享时,我们就可以利用ContentProvider为数据定义一个URI。之后 其他应用程序对数据进行查询或者修改时,只需要从当前上下文对象获得一个ContentResolver (内容解析器)传入相应的URI就可以了。本节中将以前面创建的code.db数据库为例,向读者介 绍如何定义一个ContentProvider,以及如何在其他程序中使用ContentResolver访问URI所指定 的数据。 9.3.1 定义ContentProvider 要为当前应用程序的私有数据定义URI,就需要专门定义一个继承自ContentProvider的类, 然后根据不同的操作调用的方法去实现这些方法的功能。下面我们用sqlite2这个例子,来为它 的数据库code.db定义一个URI。 首先,在sqlite2的包中创建一个新类ContryCode.java,来装入所有与数据库操作有关的静态字段,以便于打包成JAR文件供其他应用程序调用。 1 package com.studio.android.chp9.ex3; 2 3 import android.net.Uri; 4 5 public class CountryCode { 6 7 public static final String DB_NAME = "code.db"; 8 public static final String TB_NAME = "countrycode"; 9 public static final int VERSION = 1; 10 11 public static final String ID = "_id"; 12 public static final String COUNTRY = "country"; 13 public static final String CODE = "code"; 14 15 public static final String AUTHORITY = 16 "com.studio.andriod.provider.countrycode"; 17 public static final int ITEM = 1; 18 public static final int ITEM_ID = 2; 19 20 public static final String CONTENT_TYPE = 21 "vnd.android.cursor.dir/vnd.studio.android.countrycode"; 22 public static final String CONTENT_ITEM_TYPE = 23 "vnd.android.cursor.item/vnd.studio.android.countrycode"; 24 25 public static final Uri CONTENT_URI = 26 Uri.parse("content://" + AUTHORITY + "/item"); 27 } 其中DB_NAME、TB_NAME和VERSION分别定义了数据库和表的名称以及数据库的版本号。ID、 COUNTRY和CODE分别定义的是表中的各个列的列名。AUTHORITY定义了标识ContentProvider的字 符串,ITEM和ITEM_ID分别用于UriMatcher(资源标识符匹配器)中对路径item和item/id的 匹配号码。CONTENT_TYPE和CONTENT_ITEM_TYPE定义了数据的MIME类型。需要注意的是, 单一数据的MIME类型字符串应该以vnd.android.cursor.item/开头,数据集的MIME类型 字符串则应该以vnd.android.cursor.dir/开头。CONTENT_URI定义的是查询当前表数据的 content://样式URI。 接下来,同样是在sqlite2的包中创建一个继承自ContentProvider的类MyProvider.java,来 实现对数据操作的各个方法。这里将用到MyHelper来辅助获得sqliteDatabase对象,虽然也可 以直接使用Context.OpenOrCreate()方法获得,但不及使用数据库打开辅助类方便。 public class MyProvider extends ContentProvider {MyHelper dbHelper; private static final UriMatcher sMatcher; static { sMatcher = new UriMatcher(UriMatcher.NO_MATCH); sMatcher.addURI(CountryCode.AUTHORITY, "item",CountryCode.ITEM); sMatcher.addURI(CountryCode.AUTHORITY, "item/#",CountryCode.ITEM_ID); } ... } 这里UriMatcher类型的静态字段是用来匹配传入到ContentProvider中的Uri的类。其构造 方法传入的匹配码是使用match()方法匹配根路径时返回的值,这个匹配码可以为一个大于零 的数表示匹配根路径或传入-1,即常量UriMatcher.NO_MATCH表示不匹配根路径。addURI() 方法是用来增加其他URI匹配路径的,第一个参数传入标识ContentProvider的AUTHORITY字符 串。第二个参数传入需要匹配的路径,这里的#代表匹配任意数字,另外还可以用*来匹配任意 文本。第三个参数必须传入一个大于零的匹配码,用于match()方法对相匹配的URI返回相对应 的匹配码。 ContentProvider里针对数据的各种操作定义了6个抽象方法,下面是对各个方法的实现和 讲解。 @Override public boolean onCreate() { dbHelper = new MyHelper(getContext(),CountryCode.DB_NAME, null,CountryCode.VERSION); return true; } 每当ContentProvider启动时都会回调onCreate()方法。此方法主要进行一些ContentProvider 初始化的工作,返回true表示初始化成功,返回false则初始化失败。在这个ContentProvider 中,主要是构造了需要操作数据库的辅助类对象。 @Override public String getType(Uri uri) { switch (sMatcher.match(uri)) { case CountryCode.ITEM: return CountryCode.CONTENT_TYPE; case CountryCode.ITEM_ID: return CountryCode.CONTENT_ITEM_TYPE;default: throw new IllegalArgumentException("Unknown URI " + uri); } } getTyper()是用来返回数据的MIME类型的方法。使用sMatcher对URI进行匹配,并返回相 应的MIME类型字符串,若无法匹配传入的URI,抛出IllegalArgumentException异常。 @Override public int delete(Uri uri,String where,String[] args) { sqliteDatabase db = dbHelper.getWritableDatabase(); int count; switch (sMatcher.match(uri)) { case CountryCode.ITEM: count = db.delete(CountryCode.TB_NAME,where,args); break; case CountryCode.ITEM_ID: String id = uri.getPathSegments().get(1); count = db.delete(CountryCode.TB_NAME,CountryCode.ID + "=" + id + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""),args); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri,null); return count; } @Override public int update(Uri uri,ContentValues values, String where,String[] args) { sqliteDatabase db = dbHelper.getWritableDatabase(); int count; switch (sMatcher.match(uri)) { case CountryCode.ITEM: count = db.update(CountryCode.TB_NAME,values,args); break; case CountryCode.ITEM_ID: String id = uri.getPathSegments().get(1); count = db.update(CountryCode.TB_NAME,CountryCode.ID+"=" + id + (!TextUtils.isEmpty(where) ? " AND (" + where + ')' : ""),args); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } getContext().getContentResolver().notifyChange(uri,null); return count; } delete()和update()方法分别用于数据的删除和修改操作,返回的是所影响数据的数目。 我们这里两种方法的实现比较相似。首先利用数据库辅助对象获取一个sqliteDatabase对象。 然后根据传入的Uri用sMatcher进行匹配,对单个数据或数据集进行删除或修改,以便于在调用 sqliteDatabase对象的删除或修改方法时where语句中使用不同的表达式。这里通过调用 getContext()方法获得调用update()方法的Context对象,再利用这个Context对象来获取一 个ContentResolver的对象。notifyChange()方法则用来通知注册在此URI上的观察者 (observer)数据发生了改变。最后返回删除或修改数据的行数。 @Override public Uri insert(Uri uri,ContentValues initialValues) { sqliteDatabase db = dbHelper.getWritableDatabase(); long rowId; if (sMatcher.match(uri) != CountryCode.ITEM) { throw new IllegalArgumentException("Unknown URI " + uri); } rowId = db.insert(CountryCode.TB_NAME,CountryCode.ID,initialValues); if (rowId > 0) { Uri noteUri = ContentUris.withAppendedId(CountryCode.CONTENT_URI,rowId); getContext().getContentResolver().notifyChange(noteUri,null); return noteUri; } throw new sqlException("Failed to insert row into " + uri); } insert()方法用来插入数据,最后返回新插入数据的URI。在此方法的实现中,只接受数据 集的URI,即指向表的URI。然后利用数据库辅助对象获得的sqliteDatabase对象,调用insert() 方法向指定表中插入数据。最后通知观察者数据发生变化,返回插入数据的URI。 @Override public Cursor query(Uri uri,String[] projection, String selection,String[] args,String order) { sqliteDatabase db = dbHelper.getReadableDatabase(); Cursor c; switch (sMatcher.match(uri)) { case CountryCode.ITEM: c = db.query(CountryCode.TB_NAME,projection,selection, args,null,order); break; case CountryCode.ITEM_ID: String id = uri.getPathSegments().get(1); c = db.query(CountryCode.TB_NAME,CountryCode.ID + "=" + id + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""),order); break; default: throw new IllegalArgumentException("Unknown URI " + uri); } c.setNotificationUri(getContext().getContentResolver(),uri); return c; } query()是对数据进行查询的方法,最终将查询的结果包装入一个Cursor对象并返回。其实 现首先还是通过数据库辅助对象获取一个sqliteDatabase对象,然后使用sMatcher对传入URI 进行匹配,并分别对单数据和多数据的URI构造不同的where语句表达式并查询。setNotificationUri() 方法是用来为Cursor对象注册一个观察数据变化的URI。 实现完这几个抽象方法后,一个完整的ContentProvider就被定义好了,剩下的就是在Android- Manifest.xml中对这个ContentProvider的声明了。在AndroidManifest.xml中添加如下声明 代码。 <provider android:name="MyProvider" android:authorities="com.studio.andriod.provider.countrycode"/> 其中android:name需要设置成刚才定义ContentProvider类的类名,android:authorities 则指定了在content://样式的URI中标识这个ContentProvider的字符串。另外,可以设置 android:readPermission和android:writePermission这两个属性, 来分别指定对这个 ContentProvider中数据读和写操作的权限。也可以在onCreate()方法的实现中调用setRead- Permission()和setWritePermission()方法来动态指定权限。 为了让其他程序更加方便地使用我们自己定义的ContentProvider,一般会将要用到的静态数 据导出成JAR归档文件。如本程序中将所有要用到的静态字段都放在了类CountryCode中。下面 是将其打包成JAR文件的步骤。 (1) 在项目文件夹上或CountryCode.java文件上右键,选择Export...。 (2) 在弹出的选择导出类型的对话框中,点击选择JAR file,然后点Next。 (3) 在选择要导出的文件框中,只选择CountryCode.java。找到Browse...按钮指定导出路径 (默认为workspace),点选Finish完成导出CountryCode.jar。 导出了JAR文件后,若需要在其他应用程序中使用,只需要为该项目添加外部的归档文件即 可。在该项目文件夹上右键找到Build path→Add External Archive,如图9-5所示,然后选择要添 加的JAR文件就可以在程序中使用import语句导入归档文件中的内容了。 9.3.2 使用ContentResolver 查询、更改数据 1 ContentResolver类为应用程序提供了接入Content机制的方法。要构造一个Content- Resolver对象可以为构造方法ContentResolver(Context context)传入一个Context对 象,也可以直接通过Context对象调用getContentResolver()方法获得。有了Content- Resolver对象后就可以通过调用query()、insert()等方法来对数据进行操作了。下面用一 个简单的例子展示了,如何使用上一节创建的ContentProvider来对数据库code.db进行操作,如图 9-6所示。 首先,新建一个项目sqlite3。用上一节介绍的添加外部归档文件的方法添加Country- Code.jar,会在项目目录中出现Referenced Libraries→CountryCode.jar(见图9-7)。 接下来构造用户界面,修改布局文件main.xml,代码如下。 1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout 3 xmlns:android="http://schemas.android.com/apk/res/android " 4 android:orientation="vertical" 5 android:layout_width="fill_parent" 6 android:layout_height="fill_parent" 7 > 8 <TextView 9 android:layout_width="fill_parent" 10 android:layout_height="wrap_content" 11 android:text="国家" /> 12 <EditText 13 android:id="@+id/countryedit" 14 android:layout_width="fill_parent" 15 android:layout_height="wrap_content"/> 16 <TextView 17 android:layout_width="fill_parent" 18 android:layout_height="wrap_content" 19 android:text="区号" /> 20 <EditText 21 android:id="@+id/codeedit" 22 android:layout_width="fill_parent" 23 android:layout_height="wrap_content" 24 android:numeric="integer"/> 25 <Button 26 android:id="@+id/doinsert" 27 android:layout_width="fill_parent" 28 android:layout_height="wrap_content" 29 android:text="添加" /> 30 <ListView 31 android:id="@+id/displayall" 32 android:layout_width="fill_parent" 33 android:layout_height="fill_parent"/> 34 </LinearLayout> 用两个EditText分别接收用户输入的国家和区号。其中区号的EditText只接收整数输入。 使用“添加”按钮向数据库中添加数据。ListView用来显示数据库里的内容。 代码部分主要是功能的实现,编辑sqlite3.java。首先将CountryCode归档文件中的包 import进程序。 import com.studio.android.chp9.ex3.CountryCode; 在类sqlite3中定义以下实例变量。 EditText country; EditText code; Button commit; ListView list; ContentResolver resolver; 在onCreate()方法的实现中,获得各个用户界面控件对象的引用,调用getContent- Resolver()方法获得当前上下文的ContentResolver对象。用ListView以_id的升序来显示code.db中的所有信息,完成对“添加”按钮单击事件的处理,将当前EditText中的内容添加到 数据库中。 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); country = (EditText)findViewById(R.id.countryedit); code = (EditText)findViewById(R.id.codeedit); commit = (Button)findViewById(R.id.doinsert); list = (ListView)findViewById(R.id.displayall); resolver = getContentResolver(); /*填充ListView*/ Cursor c = resolver.query(CountryCode.CONTENT_URI,CountryCode.ID + " ASC"); CursorAdapter adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_2, c,new String[]{CountryCode.COUNTRY,CountryCode.CODE}, new int[]{android.R.id.text1,android.R.id.text2}); list.setAdapter(adapter); /*设置单击事件监听*/ commit.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { ContentValues values; try { String countryText = String.valueOf(country.getText()); int codeNum = Integer.valueOf(String.valueOf(code.getText())); values = new ContentValues(); values.put(CountryCode.COUNTRY,countryText); values.put(CountryCode.CODE,codeNum); } catch (Exception e) { values = new ContentValues(); } resolver.insert(CountryCode.CONTENT_URI,values); } }); } 最后重新实现onDestroy()方法。在程序退出时,删除数据库中的所有内容。@Override public void onDestroy(){ resolver.delete(CountryCode.CONTENT_URI,null); super.onDestroy(); } 在Android平台上,很多系统数据库都可以通过这种ContentProvider的机制来访问,如电 话本、多媒体文件、通话记录、短信息等,只要知道访问的URI,并且在AndroidManifest.xml 中授予相对应的可读写权限就可以很方便地对这些系统数据库进行操作了。如用一个 ListActivity显示电话本内所有联系人,如下的代码就可以实现。 public class sqlite4 extends ListActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Cursor c = getContentResolver().query(People.CONTENT_URI,null); CursorAdapter adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1,c, new String[] {People.NAME}, new int[] {android.R.id.text1}); setListAdapter(adapter); } } 由于读出联系人信息需要有android.permission.READ_CONTACTS权限,所以不要忘了在 AndroidManifest.xml中加入该权限,最后结果如图9-8所示。 <uses-permission android:name="android.permission.READ_CONTACTS" /> |