Android基础总结(6)——内容提供器
前面学习的数据持久化技术包括文件存储、SharedPreferences存储以及数据库存储技术保存的数据都只能被当前应用程序所访问。虽然文件存储和SharedPreferences存储中提供了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE这两种操作模式,可以实现不同应用程序间的数据共享,但是这两种模式在Android4.2版本中就已经被废弃了。目前,Android系统推荐使用一种更加安全可靠的内容提供器技术。
内容提供器(Content Provider)主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问的安全性。不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,内容提供器可以选择只对一部分数据进行共享,从而保证我们程序中的隐私数据不被泄露。
1、内容提供器的用法
内容提供器的用法一般有两种:
- 使用现有的内容提供器来读取和操作相应程序中的数据
- 创建自己的内容提供器给我们的程序的数据提供外部的接口
2、访问其他程序中的数据
当一个应用程序通过内容提供器对其数据提供了外部访问接口,任何其他的应用程序就都可以对这部分数据进行访问。Android系统中自带的电话簿、短信、媒体库等程序都提供了类似的访问接口,这就让第三方应用程序可以充分地利用这部分数据来实现更好的功能。
对每一个应用程序而言,如果想要访问内容提供器中的共享的数据,就一定要借助ContentResolver类,具体的步骤如下:
- 通过Context中的getContentResolver()方法获取ContentResolver类的实例对象;
- 利用ContentResolver中提供的一系列方法对数据进行CRUD操作,其中insert()方法用于插入数据,update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据。ContentResolver中的CRUD方法都是不接受表名参数,而是使用一个Uri参数代替,这个参数被称为内容URI(给内容提供器中的数据提供了唯一的标识符,它主要由两部分组成:权限+路径。其中权限用于对不同的应用程序作区分,一般为了避免冲突都会采用程序包名的方式对权限进行命名,eg:某个程序的包名为com.example.app,那么对应的权限可以命名为com.example.app.provider。路径则是用于对同一应用程序中不同的表作区分的,通常都会添加到权限的后面。eg:某个应用程序的数据库中存在两张表,table1和table2,这时可以将路径分别命名为/table1和/table2,然后将权限和路径进行组合,内容RUI就变成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.parse(String uriString)方法将URI字符串转化为Uri对象,然后将该对象传入对应的操作中来指定要操作的数据所在的表。
- 查询方法query()方法返回的仍然是一个Cursor对象,然后我们从该对象中取出查询的结果。
具体的应用示例如下:
1 //查询 2 Cursor cursor = getContentResolver().query( 3 uri, 4 projection, 5 selection, 6 selectionArgs, 7 sortOrder); 8 if (cursor != null) { 9 while (cursor.moveToNext()) { 10 String column1 = cursor.getString(cursor.getColumnIndex("column1")); 11 int column2 = cursor.getInt(cursor.getColumnIndex("column2")); 12 } 13 cursor.close(); 14 } 15 16 //添加 17 ContentValues values = new ContentValues(); 18 values.put("column1", "text"); 19 values.put("column2", 1); 20 getContentResolver().insert(uri, values); 21 22 //更新 23 ContentValues values = new ContentValues(); 24 values.put("column1", ""); 25 getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new 26 String[] {"text", "1"}); 27 28 //删除 29 getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });
下面我们尝试读取手机中的联系人的姓名和电话:
首先我们在布局中简单地设置一个按钮和一个ListView,点击按钮后读取联系人,ListView用于显示
1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 android:layout_width="match_parent" 3 android:layout_height="match_parent" 4 android:orientation="vertical" > 5 6 <Button 7 android:id="@+id/ReadContract" 8 android:layout_width="wrap_content" 9 android:layout_height="wrap_content" 10 android:text="@string/readContractButtonName"/> 11 12 <ListView 13 android:id="@+id/contract" 14 android:layout_width="wrap_content" 15 android:layout_height="wrap_content" > 16 17 </ListView> 18 19 </LinearLayout>
然后,我们在Activity的onCreate()方法为按钮添加点击事件,点击之后调用我们自己写的一个读取联系人的方法readContrast(),在该方法中我们获取系统联系人的存放的Uri,然后通过获取的ContentResolver对象的query()方法进行查询,将查询结果添加到List中,然后显示出了
1 public class MainActivity extends Activity { 2 3 private Button readContract ; 4 private ListView contractView ; 5 ArrayAdapter<String> adapter ; 6 List<String> contractsList = new ArrayList<String>() ; 7 8 @Override 9 protected void onCreate(Bundle savedInstanceState) { 10 super.onCreate(savedInstanceState); 11 setContentView(R.layout.activity_main); 12 13 readContract = (Button) findViewById(R.id.ReadContract) ; 14 contractView = (ListView) findViewById(R.id.contract) ; 15 adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contractsList) ; 16 17 readContract.setOnClickListener(new OnClickListener() { 18 @Override 19 public void onClick(View v) { 20 //contractView.setAdapter(adapter)必须在点击事件监控中设置,否则没有显示 21 contractView.setAdapter(adapter); 22 readContract() ; 23 } 24 }); 25 } 26 27 private void readContract() { 28 Cursor cursor = null ; 29 Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI ; 30 try { 31 cursor = getContentResolver().query(uri, null, null, null, null) ; 32 while(cursor.moveToNext()){ 33 String displayName = cursor.getString(cursor.getColumnIndex( 34 ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)) ; 35 String number = cursor.getString(cursor.getColumnIndex( 36 ContactsContract.CommonDataKinds.Phone.NUMBER)) ; 37 contractsList.add(displayName + "\n" + number) ; 38 } 39 }catch (Exception e){ 40 e.printStackTrace(); 41 }finally { 42 if(cursor != null){ 43 cursor.close(); 44 } 45 } 46 } 47 }
3、创建自己的内容提供器向外提供内容访问接口
如果想要实现扩程序共享数据的功能,官方推荐的方式是使用内容提供器。我们可以通过新建一个类去继承ContentProvider的方式来创建一个自己的内容提供器。ContentProvider类中有六个抽象方法,我们在使用子类继承它时,需要全部实现这六个方法:
- public boolean onCreate() :初始化的时候调用,通常在这里完成对数据库的创建和升级等操作,返回true表示内容提供器初始化成功,返回false表示初始化失败。注意,只有放存在VontentResolver尝试访问我们程序中的数据时,内容提供器才会被初始化。
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder):从内容提供器中查询数据。查询结果存放在Cursor对象中返回。
- public Uri insert(Uri uri, ContentValues values) :向内容提供器中添加一条数据。添加完成后,返回一个用于表示这条新纪录的URI。
- public int delete(Uri uri, String selection, String[] selectionArgs):从内容提供器中删除数据。被删除的行数将作为返回值返回。
- public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) :更新内容提供器中已有的数据。受影响的行数将作为返回值返回。
- public String getType(Uri uri):根据传入的内容URI来返回相应的MIME类型。
注意这些方法有与ContentResolver完全相同的方法签名。显然,这里我们需要自定义向外提供的对我们的数据的操作的实现。这样其他应用程序通过内容提供器来访问当前数据库中的共享数据的时候可以按照我们的要求得到相应的结果。所以,实现这方法时应该考虑以下事情:
- 所有的这些方法除了onCreate()以外,都能够同时被多线程调用,因此它们必须是线程安全的。
- 避免在onCreate()方法中做长时操作。直到实际需要的时候才初始化任务。有关原因会在“实现onCreate()方法”章节中进行更多的讨论。
- 尽管你必须实现这些方法,但是你的代码除了返回预定的数据类型之外,不做任何事情。例如,你可能会想阻止其他应用程序把数据插入到某些表中,你能够通过这种方法忽略对insert()方法的调用,并且返回“0”。
所以,在这里,我们首先需要解析内容URI,一个标准的URI有两种写法:
- content://com.example.app.provider/table1 :这就表示调用方期望访问的是com.example.app这个应用的table1表中的数据
- content://com.example.app.provider/table1/1 :这就表示调用方期望访问的是com.example.app这个应用的table1表中id为1的数据
所以解析内容URI的目的就是要弄清楚所访问内容URI到底是哪一种,从而确定我们要访问的数据是表格的所有数据还是部分数据。以路径结尾就表示期望访问该表中所有的数据,以id结尾就表示期望访问该表中拥有相应id的数据。我们可以使用通配符的方式来分别匹配这两种格式的内容URI,规则如下:
-
*:表示匹配任意长度的任意字符
-
# :表示匹配任意长度的数字
所以,
- 一个能够匹配任意表的内容URI格式就可以写出:
content://com.example.app.provider/*
- 一个能够匹配table1表中任意一行数据的内容URI格式就可以写成:
content://com.example.app.provider/table1/#
然后我们可以借助URIMatcher可以实现内容URI的匹配。URIMatcher提供了addURI(),接受三个参数(权限.路径,自定义代码)。返回一个匹配这个人Uri对象所对应的自定义代码。根据代码可知调用方访问的是那张表的数据。
1 public class MyProvider extends ContentProvider { 2 3 /* 4 * MyProvider中新增四个整形常量,其中TABLE1_DIR表示访问table1表中的所有数据, 5 * TABLE1_ITEM表示访问的table1表中的单条数据,TABLE2_DIR表示访问table2表中的所有数据, 6 * TABLE2_ITEM表示访问的table2表中的单条数据。 7 */ 8 public static final int TABLE1_DIR = 0; 9 public static final int TABLE1_ITEM = 1; 10 public static final int TABLE2_DIR = 2; 11 public static final int TABLE2_ITEM = 3; 12 private static UriMatcher uriMatcher; 13 14 /* 15 * 上面定义常量以后,接着在静态代码块里,创建UriMatcher的实例,并调用addURI()方法,将期望匹配的内容URI格式传递进去, 16 * 注意这里传入的路径参数是可以使用通配符的。所以,对于隐式数据,我们不匹配进来就可以避免让其他应用程序进行访问。然后当query()方法被调用的时候, 17 * 就会通过UriMatcher的match()方法对传入的Uri对象进行匹配,如果发现UriMatcher中某个内容URI格式成功匹配了该Uri对象,则 18 * 返回相应的自定义代码,然后就可以判断期望访问的到底是什么数据了。这里只使用query()方法做了一个示范,其实 19 * insert(),update(),delete()这几个方法的实现也是差不多的,它们都会携带Uri这个参数,然后同样利用 20 * UriMatcher的match()方法判断出调用期望访问的是哪一张表,在对该表中的数据进行相应的操作就可以了。 21 */ 22 static { 23 uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 24 uriMatcher.addURI("com.jack.contactstest.provider", "table1", TABLE1_DIR); 25 uriMatcher.addURI("com.jack.contactstest.provider", "table1/#", TABLE1_ITEM); 26 uriMatcher.addURI("com.jack.contactstest.provider", "table2", TABLE2_DIR); 27 uriMatcher.addURI("com.jack.contactstest.provider", "table2/#", TABLE2_ITEM); 28 } 29 30 /* 31 * onCreate()方法初始化内容提供器的时候调用。通常会在这里完成对数据库的创建和升级等操作,返回 32 * true表示内容提供器初始化成功,返回false则表示失败。注意只有当存在ContentResolver尝试访问 33 * 我们程序中的数据时,内容提供器才会被初始化。 34 */ 35 @Override 36 public boolean onCreate() { 37 // TODO Auto-generated method stub 38 return false; 39 } 40 41 /* 42 * query()方法从内容提供器中查询数据。使用uri参数来确定查询哪张表,projection参数用于确定查询 43 * 哪些列,selection和selectionArgs参数用于约束查询哪些行,sortOrder参数用于对结果进行排序, 44 * 查询的结果存放在Cursor对象中返回。 45 */ 46 @Override 47 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { 48 // TODO Auto-generated method stub 49 switch (uriMatcher.match(uri)) { 50 case TABLE1_DIR: 51 // 查询table1表中的所有数据 52 break; 53 case TABLE1_ITEM: 54 // 查询table1表中的单条数据 55 break; 56 case TABLE2_DIR: 57 // 查询table2表中的所有数据 58 break; 59 case TABLE2_ITEM: 60 // 查询table2表中的单条数据 61 break; 62 } 63 return null; 64 } 65 。。。 66 }
到这里,一个完整的内容提供器就创建完成了,现在任何一个应用程序都可以使用ContentResolver来访问我们程序中的数据。那么如何才能保证隐私数据不会泄漏出去呢?其实多亏了内容提供器的良好机制,这个问题已经已经在不知不觉中被解决了。因为所有的CRUD操作都一定要匹配到相应的内容URI格式才能进行,而我们当然不可能向UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。
实现各方法应该注意的问题:
实现query()方法
ContentProvider.query()方法必须返回一个Cursor对象,如果执行失败,就抛出一个异常。如果你正在使用一个SQLite数据库做为你的数据存储,你能够通过调用SQLiteDatabase类的一个query()方法,就能简单的返回Cursor对象。如果查询不到匹配的行,那么返回的Cursor对象的getCount()方法就会返回0。只有在查询过程期间发生了内部错误,你才应该返回null。
如果你不适用SQLite数据库做为数据存储,那么就要使用Cursor的一个具体子类。如,MatrixCursor类实现了每行是一个对象数组的游标,这个类用addRow()方法来添加新行。
记住,Android系统必须能够跨进程来传递异常。在处理的查询错误中,下列异常可用于进程间传递:
- IllegalArgumentException(如果提供器收到一个无效的内容资源标识,可以选择抛出这个异常)
- NullPointerException
实现insert()方法
insert()方法使用ContentValues参数中的值把一行新的数据添加到相应的表中。如果在ContentValues参数中没有列名,你可能是想要使用提供器代码或数据库模式来提供默认值。
这个方法应该返回新插入行的内容资源标识。使用withAppendedId()方法给这个新行追加一个_ID(或其他主键)值。
实现delete()方法
不要使用delete()方法从你的数据存储中物理的删除行。因为如果你的提供器使用了同步适配器,你就应该使用“delete”标识来标记要删除的行,而不是把完全的删除行。同步适配器会在从提供器中删除它们之前检查要删除的行,并且从服务端删除它们。
实现update()方法
Update()方法需要与insert()方法使用的相同的ContentValues参数,以及与delete()方法和query()方法相同的selection和selectionArgs参数。这种方法允许你在这些方法之间重用代码。
getType()是所有内容提供器都必须提供的方法.用于获取Uri对象对应的MIME类型.MINE由三部分组成:
- 必须以 vnd 开头。
- 如果内容 URI 以路径结尾,则后接 android.cursor.dir/,如果内容 URI 以 id 结尾,则后接 android.cursor.item/。
- 最后接上 vnd.<authority>.<path>。
示例如下,两种不同类型的内容URI的返回的类型如下:
- content://com.example.app.provider/table1
==>vnd.android.cursor.dir/vnd.com.example.app.provider.table1 - content://com.example.app.provider/table1/1 ==>vnd.android.cursor.item/vnd. com.example.app.provider.table1
对于隐私数据不提供对应的URI外部程序就无法访问了.
实现onCreate()方法
Android系统会在提供器启动时调用onCreate()方法。你只应该在这个方法中执行快速的初始任务,并且要把数据库的创建和数据的装载延迟到提供器接收到实际的数据请求之后。如果在你onCreate()方法中你执行了长时任务,会降低提供器的启动速度,从而降低提供器对其他应用程序的响应速度。
例如,如果你使用SQLite数据库,而且在onCreate()方法中创建了一个新的SQLiteOpenHelper对象,然后在首次打开数据时,创建SQL表。要做这项工作,首先调用getWritableDatabase()方法,它会自动的调用SQLiteOpenHelper.onCreate()方法。
以下两段代码演示了ContentProvider.onCreate()方法和SQLiteOpenHelper.onCreate()方法之间的交互。
第一段代码时ContentProvider.onCreate()方法的实现。
1 public class ExampleProvider extends ContentProvider 2 3 /* 4 * Defines a handle to the database helper object. The MainDatabaseHelper class is defined 5 * in a following snippet. 6 */ 7 private MainDatabaseHelper mOpenHelper; 8 9 // Defines the database name 10 private static final String DBNAME = "mydb"; 11 12 // Holds the database object 13 private SQLiteDatabase db; 14 15 public boolean onCreate() { 16 17 /* 18 * Creates a new helper object. This method always returns quickly. 19 * Notice that the database itself isn't created or opened 20 * until SQLiteOpenHelper.getWritableDatabase is called 21 */ 22 mOpenHelper = new SQLiteOpenHelper( 23 getContext(), // the application context 24 DBNAME, // the name of the database) 25 null, // uses the default SQLite cursor 26 1 // the version number 27 ); 28 29 return true; 30 } 31 32 ... 33 34 // Implements the provider's insert method 35 public Cursor insert(Uri uri, ContentValues values) { 36 // Insert code here to determine which table to open, handle error-checking, and so forth 37 38 ... 39 40 /* 41 * Gets a writeable database. This will trigger its creation if it doesn't already exist. 42 * 43 */ 44 db = mOpenHelper.getWritableDatabase(); 45 } 46 }
以下代码时是SQLiteOpenHelper.onCreate()方法的实现,而且包括了一个助手类:
// A string that defines the SQL statement for creating a table private static final String SQL_CREATE_MAIN = "CREATE TABLE " + "main " + // Table's name "(" + // The columns in the table " _ID INTEGER PRIMARY KEY, " + " WORD TEXT" " FREQUENCY INTEGER " + " LOCALE TEXT )"; ... /** * Helper class that actually creates and manages the provider's underlying data repository. */ protected static final class MainDatabaseHelper extends SQLiteOpenHelper { /* * Instantiates an open helper for the provider's SQLite data repository * Do not do database creation and upgrade here. */ MainDatabaseHelper(Context context) { super(context, DBNAME, null, 1); } /* * Creates the data repository. This is called when the provider attempts to open the * repository and SQLite reports that it doesn't exist. */ public void onCreate(SQLiteDatabase db) { // Creates the main table db.execSQL(SQL_CREATE_MAIN); } }
下面进行实战,体验一下跨程序共享的功能。 简单起见,我们使用上一篇博客的DatabaseTest的项目,在该项目的基础上进行修改继续开发,通过内容提供器给它加入外部访问接口。打开DatabaseTest项目,首先将MyDatabaseHelper中使用Toast弹出创建数据成功的提示去掉,因为跨程序访问时我们不能直接使用Toast。然后添加一个DatabaseProvider类,代码如下所示:
1 public class DatabaseProvider extends ContentProvider { 2 3 //自定义代码 4 public static final int BOOK_DIR=0; 5 public static final int BOOK_ITEM=1; 6 public static final int CATEGORY_DIR=2; 7 public static final int CATEGORY_ITEM=3; 8 //权限 9 public static final String AUTHORITY="com.jack.databasetest.provider"; 10 private static UriMatcher uriMatcher; 11 private MyDatabaseHelper dbHelper; 12 //静态代码块进行初始话 13 static { 14 uriMatcher=new UriMatcher(UriMatcher.NO_MATCH); 15 uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR); 16 uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM); 17 uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR); 18 uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM); 19 } 20 21 22 23 @Override 24 public int delete(Uri uri, String selection, String[] selectionArgs) { 25 // TODO Auto-generated method stub 26 //删除数据 27 SQLiteDatabase db=dbHelper.getWritableDatabase(); 28 int deleteRows=0; 29 switch(uriMatcher.match(uri)){ 30 case BOOK_DIR: 31 deleteRows=db.delete("book", selection, selectionArgs); 32 break; 33 case BOOK_ITEM: 34 String bookId=uri.getPathSegments().get(1); 35 deleteRows=db.delete("book", "id=?", new String[]{bookId}); 36 break; 37 case CATEGORY_DIR: 38 deleteRows=db.delete("category", selection, selectionArgs); 39 break; 40 case CATEGORY_ITEM: 41 String categoryId=uri.getPathSegments().get(1); 42 deleteRows=db.delete("category", "id=?",new String[]{categoryId}); 43 break; 44 default: 45 break; 46 } 47 return deleteRows;//被删除的行数作为返回值返回 48 } 49 50 @Override 51 public String getType(Uri uri) { 52 // TODO Auto-generated method stub 53 switch(uriMatcher.match(uri)){ 54 case BOOK_DIR: 55 return "vnd.android.cursor.dir/vnd.com.jack.databasetest.provider.book"; 56 case BOOK_ITEM: 57 return "vnd.android.cursor.item/vnd.com.jack.databasetest.provider.book"; 58 case CATEGORY_DIR: 59 return "vnd.android.cursor.dir/vnd.com.jack.databasetest.provider.category"; 60 case CATEGORY_ITEM: 61 return "vnd.android.cursor.item/vnd.com.jack.databasetest.provider.category"; 62 } 63 return null; 64 } 65 66 @Override 67 public Uri insert(Uri uri, ContentValues values) { 68 // TODO Auto-generated method stub 69 //添加数据 70 SQLiteDatabase db=dbHelper.getWritableDatabase(); 71 Uri uriReturn=null; 72 switch(uriMatcher.match(uri)){ 73 case BOOK_DIR: 74 case BOOK_ITEM: 75 long newBookId=db.insert("book", null, values); 76 uriReturn=Uri.parse("content://"+AUTHORITY+"/book/"+newBookId); 77 break; 78 case CATEGORY_DIR: 79 case CATEGORY_ITEM: 80 long newCategoryId=db.insert("category", null, values); 81 uriReturn=Uri.parse("content://"+AUTHORITY+"/book/"+newCategoryId); 82 break; 83 default: 84 break; 85 } 86 /* 87 * insert()方法要求返回一个能够表示这条新增数据的URI,所以需要调用Uri.parse()方法来将一个内容 88 * URI解析成Uri对象,当然这个内容是以新增数据的id结尾的。 89 * */ 90 return uriReturn; 91 } 92 93 @Override 94 public boolean onCreate() { 95 // TODO Auto-generated method stub 96 dbHelper=new MyDatabaseHelper(getContext(), "BookStore.db", null, 2); 97 return true;//返回true表示内容提供器初始化成功,这时数据库就已经完成了创建或升级操作 98 } 99 100 @Override 101 public Cursor query(Uri uri, String[] projection, String selection, 102 String[] selectionArgs, String sortOrder) { 103 // TODO Auto-generated method stub 104 //查询数据 105 SQLiteDatabase db=dbHelper.getReadableDatabase();//获得SQLiteDatabase对象 106 Cursor cursor=null; 107 switch(uriMatcher.match(uri)){ 108 case BOOK_DIR: 109 //进行查询 110 cursor=db.query("book", projection, selection, selectionArgs, 111 null, null, sortOrder); 112 break; 113 case BOOK_ITEM: 114 //进行查询 115 /*Uri对象的getPathSegments()方法会将内容URI权限之后的部分以“、”符号进行分割,并把分割后的结果 116 * 放入到一个字符串列表中,那这个列表的第0个位置存放的就是路径,第1个位置存放的就是id,得到id后,在通过 117 * selection和selectionArgs参数就实现了查询单条数据的功能。 118 * */ 119 String bookId=uri.getPathSegments().get(1); 120 cursor=db.query("book", projection, "id=?", new String[]{bookId}, 121 null, null, sortOrder); 122 break; 123 case CATEGORY_DIR: 124 //进行查询 125 cursor=db.query("category", projection, selection, selectionArgs, 126 null, null, sortOrder); 127 break; 128 case CATEGORY_ITEM: 129 //进行查询 130 String categoryId=uri.getPathSegments().get(1); 131 cursor=db.query("book", projection, "id=?", new String[]{categoryId}, 132 null, null, sortOrder); 133 break; 134 default: 135 break; 136 } 137 return cursor; 138 } 139 140 @Override 141 public int update(Uri uri, ContentValues values, String selection, 142 String[] selectionArgs) { 143 // TODO Auto-generated method stub 144 SQLiteDatabase db=dbHelper.getWritableDatabase(); 145 int updatedRows=0; 146 //更新数据 147 switch(uriMatcher.match(uri)){ 148 case BOOK_DIR: 149 updatedRows=db.update("book", values, selection,selectionArgs); 150 break; 151 case BOOK_ITEM: 152 String bookId=uri.getPathSegments().get(1); 153 updatedRows=db.update("book", values, "id=?", new String[]{bookId}); 154 break; 155 case CATEGORY_DIR: 156 updatedRows=db.update("category", values, selection,selectionArgs); 157 break; 158 case CATEGORY_ITEM: 159 String categoryId=uri.getPathSegments().get(1); 160 updatedRows=db.update("book", values, "id=?", new String[]{categoryId}); 161 break; 162 default: 163 break; 164 } 165 return updatedRows;//受影响的行数作为返回值 166 } 167 168 }
上面的功能,在注释已经说名了,就不多说了,经过上面的步骤,内容提供器的代码全部编写完了,不过离跨实现程序数据共享的功能还差了一小步,因为还需要将内容提供器在AndroidManifest.xml文件中注册才可以,如下所示:
1 <pre name="code" class="html"><?xml version="1.0" encoding="utf-8"?> 2 <manifest xmlns:android="http://schemas.android.com/apk/res/android" 3 package="com.jack.databasetest" 4 android:versionCode="1" 5 android:versionName="1.0" > 6 7 <uses-sdk 8 android:minSdkVersion="13" 9 android:targetSdkVersion="17" /> 10 11 <application 12 android:allowBackup="true" 13 android:icon="@drawable/ic_launcher" 14 android:label="@string/app_name" 15 android:theme="@style/AppTheme" > 16 <activity 17 android:name="com.jack.databasetest.MainActivity" 18 android:label="@string/app_name" > 19 <intent-filter> 20 <action android:name="android.intent.action.MAIN" /> 21 22 <category android:name="android.intent.category.LAUNCHER" /> 23 </intent-filter> 24 </activity> 25 26 27 <provider 28 android:name="com.jack.databasetest.DatabaseProvider" 29 android:authorities="com.jack.databasetest.provider" 30 android:exported="true" 31 ></provider> 32 33 </application> 34 35 </manifest>
android:exported=”true”这个属性,值加了上面的android:name=”com.jack.databasetest.DatabaseProvider” android:authorities=”com.jack.databasetest.provider”属性,程序访问出现安全问题了,百度后,说是需要android:exported=”true”这个属性,才能跨程序被其他的程序访问。我试试了下,当中需要这个属性,不然后面进行跨程序访问的时候会出现错误。
现在DatabaseTest这个项目就已经拥有了跨程序共享数据的功能了,现在我们来试试。首先需要将DatabaseTest程序从模拟器中删除掉,以防止以前的遗留数据对我们产生影响。然后运行下项目,将DatabaseTest程序重写安装在模拟器上。接着关闭这个项目,并创建一个新项目ProviderTest,我们就通过这个程序去访问DatabaseTest中的数据。
先修改下ProviderTest的布局文件activity_main.xml中的代码,如下所示:
1 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 2 xmlns:tools="http://schemas.android.com/tools" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:orientation="vertical" 6 > 7 8 <Button 9 android:id="@+id/add_data" 10 android:layout_width="match_parent" 11 android:layout_height="wrap_content" 12 android:text="add data to book" 13 /> 14 15 <Button 16 android:id="@+id/query_data" 17 android:layout_width="match_parent" 18 android:layout_height="wrap_content" 19 android:text="query from book" 20 /> 21 22 <Button 23 android:id="@+id/update_data" 24 android:layout_width="match_parent" 25 android:layout_height="wrap_content" 26 android:text="update book" 27 /> 28 29 <Button 30 android:id="@+id/delete_data" 31 android:layout_width="match_parent" 32 android:layout_height="wrap_content" 33 android:text="delete data from book" 34 /> 35 36 37 </LinearLayout>
放置了四个按钮,分别用来添加数据,查询,修改和删除数据。然后在修改MainActivity中的代码,如下所示:
1 package com.jack.providertest; 2 3 import android.net.Uri; 4 import android.os.Bundle; 5 import android.app.Activity; 6 import android.content.ContentValues; 7 import android.database.Cursor; 8 import android.util.Log; 9 import android.view.Menu; 10 import android.view.View; 11 import android.view.View.OnClickListener; 12 import android.widget.Button; 13 14 public class MainActivity extends Activity { 15 16 private String newId; 17 @Override 18 protected void onCreate(Bundle savedInstanceState) { 19 super.onCreate(savedInstanceState); 20 setContentView(R.layout.activity_main); 21 22 Button addData=(Button) findViewById(R.id.add_data); 23 addData.setOnClickListener(new OnClickListener() { 24 25 @Override 26 public void onClick(View v) { 27 // TODO Auto-generated method stub 28 //添加数据 29 Uri uri=Uri.parse("content://com.jack.databasetest.provider/book"); 30 ContentValues values=new ContentValues(); 31 values.put("name", "a clash of kings"); 32 values.put("author", "george martin"); 33 values.put("pages", 1050); 34 values.put("price", 88.9); 35 Uri newUri=getContentResolver().insert(uri, values);//插入数据 36 newId=newUri.getPathSegments().get(1); 37 } 38 }); 39 40 41 Button queryData=(Button) findViewById(R.id.query_data); 42 queryData.setOnClickListener(new OnClickListener(){ 43 44 @Override 45 public void onClick(View v) { 46 // TODO Auto-generated method stub 47 //查询数据 48 Uri uri=Uri.parse("content://com.jack.databasetest.provider/book"); 49 Cursor cursor=getContentResolver().query(uri, null, null, null, null); 50 if(cursor!=null){ 51 while(cursor.moveToNext()){ 52 String name=cursor.getString(cursor.getColumnIndex("name")); 53 String author=cursor.getString(cursor.getColumnIndex("author")); 54 int pages=cursor.getInt(cursor.getColumnIndex("pages")); 55 double price=cursor.getDouble(cursor.getColumnIndex("price")); 56 57 Log.d("MainActivity", "book name is "+name); 58 Log.d("MainActivity", "book author is "+author); 59 Log.d("MainActivity", "book pages is "+pages); 60 Log.d("MainActivity", "book price is "+price); 61 } 62 cursor.close(); 63 } 64 } 65 66 }); 67 68 69 Button updateData=(Button) findViewById(R.id.update_data); 70 updateData.setOnClickListener(new OnClickListener(){ 71 72 @Override 73 public void onClick(View v) { 74 // TODO Auto-generated method stub 75 //更新数据 76 Uri uri=Uri.parse("content://com.jack.databasetest.provider/book/"+newId); 77 ContentValues values=new ContentValues(); 78 values.put("name", "a storm of swords"); 79 values.put("pages", 1216); 80 values.put("price", 77.8); 81 getContentResolver().update(uri, values, null, null); 82 } 83 84 }); 85 86 87 Button deleteData=(Button) findViewById(R.id.delete_data); 88 deleteData.setOnClickListener(new OnClickListener(){ 89 90 @Override 91 public void onClick(View v) { 92 // TODO Auto-generated method stub 93 //删除数据 94 Uri uri=Uri.parse("content://com.jack.databasetest.provider/book/"+newId); 95 getContentResolver().delete(uri, null, null); 96 } 97 98 }); 99 102 } 103 104 @Override 105 public boolean onCreateOptionsMenu(Menu menu) { 106 // Inflate the menu; this adds items to the action bar if it is present. 107 getMenuInflater().inflate(R.menu.main, menu); 108 return true; 109 } 110 111 }
现在运行下ProviderTest项目,显示如下:
点击add data to book,此时数据应该已经添加到DatabaseTest程序的数据库中了 ,我们通过点击query form book按钮来检查下,打印日志如下:
然后点击下update book按钮来更新数据,在点击下query from book按钮进行检查,结果如下: