- 为什么要有内容提供者
- 内容提供者的工作原理
- 使用内容解析者对内容提供者进行增删改查操作
- 利用内容提供者和内容解析者备份手机短信
- 利用内容提供者插入短信
为什么要有内容提供者
- 内容提供者技术的目的是:
- 把私有数据库的数据的内容暴露给外部使用;
我们知道,微信、QQ等应用都可以读取手机中联系人和短信的数据。而联系人和短信都是系统内置的应用,它们的数据都存储在相应的数据库中。
在com.android.provider.telephony/databases/mmssms.db
就是短信的数据库,通过下面的图可以知道mmssms.db
的权限为-rw-rw---
,也就是其他用户不可以直接访问,是私有的。
同样的com.android.provider.contacts/databases/contact2.db
是联系人的数据库,同样也是私有的权限。
既然它们都是私有的,但是又有需求获取里面的数据应该怎么办?Google提供者了内容提供者的技术,就可以解决这一需求。
内容提供者的工作原理
由于数据库是私有的,外部应用不可以直接访问。那么便在应用内部使用内容提供者,向外部暴露接口,这样外部可以借助“中间人”访问到内部的数据库,原理图如下:
- 内容提供者的编写步骤如下:
- ①创建数据库、表,因为内容提供者主要是把私有数据库数据提供给外部访问。
- ②写一个类继承
ContentProvider
类,并重写其中的onCreate()
、query()
、insert()
、update()
、delete()
、getType()
等方法。 - ③确定主机名(
authority
),添加路径匹配规则,此处会利使用到UriMatcher
类。 - ④在清单文件中声明内容提供者,并添加关键属性
android:authorities
- ⑤编写内容提供者的
onCreate()
、query()
、insert()
、update()
、delete()
等方法。
接下来就一步一步把所有步骤说清。
第一步,创建数据库、表。很简单写一个继承SQliteOpenHelper
得类,并创建数据库。代码如下:
public class MySQLiteOpenHelper extends SQLiteOpenHelper { public MySQLiteOpenHelper(Context context) {super(context, "account.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { // 创建数据库,并初始化两条语句 db.execSQL("create table account (_id integer primary key autoincrement,name varchar(20),money varchar(20))"); db.execSQL("insert into account ('name','money') values ('张三','2000')"); db.execSQL("insert into account ('name','money') values ('李四','5000')"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { }}
第二步,写一个类继承ContentProvider
类,并重写其中方法。主要都是一些骨架的代码,也没什么难度。
public class CopyOfAccountContentProvider extends ContentProvider { // 内容提供者初始化的时候调用 @Override public boolean onCreate() { return true; } // 提供给外部应用调用的查询方法 @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; } // 提供给外部应用调用的增加方法 @Override public Uri insert(Uri uri, ContentValues values) { return null; } // 提供给外部应用调用的删除方法 @Override public int delete(Uri uri, String selection, String[] selectionArgs) { return 0; } // 提供给外部应用调用的更新方法 @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { return 0; } @Override public String getType(Uri uri) { // 没有什么用,此处就不写了 return null; }}
第三步,从第三步开始就比较关键了。要确定主机名(authority
),添加路径匹配规则,此处是使用内容解析者时,调用其他应用私有数据库所使用的。代码如下,对了这些代码是写在内容提供者类中的:
public static final int QUERY_SUCCESS = 0;public static final int UPDATE_SUCCESS = 1;public static final int DELETE_SUCCESS = 2;public static final int INSERT_SUCCESS = 3;// UriMatcher是一个工具类,用于帮助内容提供者匹配URIsprivate static final UriMatcher MURI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);// 添加路径匹配规则static { // 添加一个path(匹配规则),如果匹配成功会返回code值。 // authority:主机名(我们可以自己定义,随意写,当使用内容解析者时会使用到) // path:路径,一般是操作名称 // code:响应码 MURI_MATCHER.addURI("com.bzh.account.contentprovider", "insert", INSERT_SUCCESS); MURI_MATCHER.addURI("com.bzh.account.contentprovider", "delete", DELETE_SUCCESS); MURI_MATCHER.addURI("com.bzh.account.contentprovider", "update", UPDATE_SUCCESS); MURI_MATCHER.addURI("com.bzh.account.contentprovider", "query", QUERY_SUCCESS);}
第四步,内容提供者是四大组件之一,四大组件都需要在清单文件中声明。在清单文件中声明内容提供者,并添加关键属性android:authorities
,这个属性的内容和添加路径匹配规则addURI()
的第一个参数必须一致。代码如下:
<provider android:name="com.bzh.contentprovider.AccountContentProvider" android:authorities="com.bzh.account.contentprovider" ></provider>
第五步,编写内容提供者的onCreate()
、query()
、insert()
、update()
、delete()
等方法。这些方法的内容都是使用SQLiteDatabase
内部的方法直接调用,也比较简单。
private SQLiteDatabase db;private static final String ACCOUNT = "account";// 内容提供者初始化的时候调用@Overridepublic boolean onCreate() { MySQLiteOpenHelper mySQLiteOpenHelper = new MySQLiteOpenHelper(getContext()); db = mySQLiteOpenHelper.getWritableDatabase(); return true;}// 提供给外部应用调用的查询方法@Overridepublic Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { int MATCH_CODE = MURI_MATCHER.match(uri); // 判断路径是否匹配成功 if (MATCH_CODE == QUERY_SUCCESS) { // 调用SQLiteDatabase中的方法查询私有的数据库 return db.query(ACCOUNT, projection, selection, selectionArgs, null, null, null); } return null;}// 提供给外部应用调用的增加方法@Overridepublic Uri insert(Uri uri, ContentValues values) { int MATCH_CODE = MURI_MATCHER.match(uri); // 判断路径是否匹配成功 if (MATCH_CODE == INSERT_SUCCESS) { long insertResult = db.insert(ACCOUNT, null, values); // 此处的Uri可以随意写 return Uri.parse("com.bzh.account.contentprovider-result:" + insertResult); } return null;}// 提供给外部应用调用的删除方法@Overridepublic int delete(Uri uri, String selection, String[] selectionArgs) { int MATCH_CODE = MURI_MATCHER.match(uri); // 判断路径是否匹配成功 if (MATCH_CODE == DELETE_SUCCESS) { return db.delete(ACCOUNT, selection, selectionArgs); } return 0;}// 提供给外部应用调用的更新方法@Overridepublic int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int MATCH_CODE = MURI_MATCHER.match(uri); // 判断路径是否匹配成功 if (MATCH_CODE == UPDATE_SUCCESS) { return db.update(ACCOUNT, values, selection, selectionArgs); } return 0;}@Overridepublic String getType(Uri uri) { // 没有什么用,此处就不写了 return null;}
至此,整个内容提供者就编写完毕了,接下就是如何使用了,具体如何使用将在下一节讲述。
使用内容解析者对内容提供者进行增删改查操作
回想一下上面的原理图,访问私有数据库的内容提供者已经准备完毕,家下来是使用内容解析者间接调用数据库内容的时候了。
相对于创建内容提供者而言,使用解析者调用提供者提供的方法就简单很多了。
创建一个工程,并添加几个增删改查的按钮,布局之类的比较简单不贴了,请看图:
接下来要做的事情就是,当点击按钮时调用内容提供者对应的方法。其中最值得注意的是Uri.parse()
内容的写法。
Uri uri = Uri.parse("content://com.bzh.account.contentprovider/query");
对于这句内容而言,主要由三部分组成–①content://②主机地址/③路径
,而这部分是固定的写法,其中主机地址是内容提供者清单文件中声明的authorities
属性;而路径是内容提供者中UriMatcher.addURI()
第二个参数对应的值。
下面是代码:
public void insert(View v) { // 获取到内容解析者 ContentResolver resolver = getContentResolver(); // 匹配路径和数据 Uri url = Uri.parse("content://com.bzh.account.contentprovider/insert"); ContentValues values = new ContentValues(); values.put("name", "别志华"); values.put("money", "999"); // 使用解析者借助内容提供者向私有数据库插数据 Uri insert = resolver.insert(url, values); System.out.println("数据插入---结果:" + insert.toString());}public void delete(View v) { // 获取到内容解析者 ContentResolver resolver = getContentResolver(); // 匹配路径和数据 Uri url = Uri.parse("content://com.bzh.account.contentprovider/delete"); // 使用解析者借助内容提供者向私有数据库插数据 int delete = resolver.delete(url, "name=?", new String[] { "别志华" }); System.out.println("删除数据---影响行数:" + delete);}public void update(View v) { // 获取到内容解析者 ContentResolver resolver = getContentResolver(); // 匹配路径和数据 Uri uri = Uri.parse("content://com.bzh.account.contentprovider/update"); ContentValues values = new ContentValues(); values.put("money", "999"); // 使用解析者借助内容提供者向私有数据库插数据 int update = resolver.update(uri, values, "name=?", new String[] { "别志华" }); System.out.println("更新数据---影响行数:" + update);}public void query(View v) { // 获取内容解析者 ContentResolver resolver = getContentResolver(); // com.bzh.account.contentprovider是在上一个应用清单文件中authorities的属性值 // 固定写法:content://主机地址/路径 Uri uri = Uri.parse("content://com.bzh.account.contentprovider/query"); // 调用内容解析者的查询方法 Cursor cursor = resolver.query(uri, null, null, null, null); // 拿出游标中的数据 if (cursor != null && cursor.getCount() > 0) { while (cursor.moveToNext()) { String name = cursor.getString(1); String money = cursor.getString(2); System.out.println("查询数据---数据内容(姓名:" + name + ",钱:" + money + ")"); } // 关闭游标 cursor.close(); }}
测试图如下:
到此为止,如何定义一个内容提供者和如何使用内容提供者已经讲解完了。
利用内容提供者和内容解析者备份手机短信
像“微信电话薄”应用,可以读取手机中的短信数据。我们知道,手机中的短信应用内部维护者一个数据库,用于存储短信数据。而“微信电话薄”就是读取系统短信应用的数据来达到自己的目的的,但是系统短信应用的数据库是私有的,那么外部的应用如何拿到私有数据库的数据?你想的没错,Google工程师在短信应用中为我们提供了内容提供者,供我们开发人员使用。
在上一节中,我们知道想要使用其他应用提供的内容提供者,需要知道主机名和路径,那么从哪里获取这些信息呢?这就需要去查看源码了,我这里查看的是4.4的源码。源码的路径为android4.4\packages\providers\TelephonyProvider
。
目录结构如下图:
在上一节中,写自己的提供提供者时都是在清单文件中声明内容提供者,并声明主机名,让我们来看一下吧。
<provider android:name="SmsProvider" android:authorities="sms" android:multiprocess="false" android:exported="true" android:readPermission="android.permission.READ_SMS" android:writePermission="android.permission.WRITE_SMS" />
主机名是sms
,并且从中可一知道使用该内容提供者,需要提供读取短信和写短信的权限。
知道了主机名,接下来就是去查看路径了。找到src\com\android\providers\telephony\SmsProvider.java
文件,并搜索UriMatcher
关键字,可以得到如下的结果:
private static final UriMatcher sURLMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { sURLMatcher.addURI("sms", null, SMS_ALL); sURLMatcher.addURI("sms", "#", SMS_ALL_ID); sURLMatcher.addURI("sms", "inbox", SMS_INBOX); sURLMatcher.addURI("sms", "inbox/#", SMS_INBOX_ID); sURLMatcher.addURI("sms", "sent", SMS_SENT); sURLMatcher.addURI("sms", "sent/#", SMS_SENT_ID); sURLMatcher.addURI("sms", "draft", SMS_DRAFT); sURLMatcher.addURI("sms", "draft/#", SMS_DRAFT_ID); ... ... ... }
知道了主机名和路径后,在看一下短信数据库的表结构,要不然我们怎么获取数据!从中可以发现在sms表中address
、date
、body
分别代表着发送人、时间、短信内容;
接下来,使用内容解析者查询数据并使用XML序列化器,把数据序列化到XML文件中,短信备份就算完成了,代码如下:
// 内容解析者ContentResolver resolver = getContentResolver();Uri uri = Uri.parse("content://sms");Cursor cursor = resolver.query(uri, new String[] { "address", "date", "body" }, null, null, null);if (cursor != null && cursor.getCount() > 0) { // XML序列化器 XmlSerializer serializer = Xml.newSerializer(); FileOutputStream fos; try { fos = new FileOutputStream(new File(Environment.getExternalStorageDirectory().getPath() + "/backsms.xml")); // 初始化XML序列化器参数 serializer.setOutput(fos, "UTF-8"); // 文档开始 serializer.startDocument("UTF-8", true); // 根节点开始 serializer.startTag(null, "smss"); while (cursor.moveToNext()) { // 元素结点开始 serializer.startTag(null, "sms"); // 序列化发送人 serializer.startTag(null, "address"); String address = cursor.getString(0); serializer.text(address); serializer.endTag(null, "address"); serializer.startTag(null, "date"); String date = cursor.getString(1); serializer.text(date); serializer.endTag(null, "date"); serializer.startTag(null, "body"); String body = cursor.getString(2); serializer.text(body); serializer.endTag(null, "body"); System.out.println("短信发送人:" + address + ",日期:" + date + ",内容:" + body); serializer.endTag(null, "sms"); } serializer.endTag(null, "smss"); serializer.endDocument(); } catch (Exception e) { e.printStackTrace(); } cursor.close();}
利用内容提供者插入短信
做完备份短信,再做一个插入短信玩玩吧。
首先提供一个界面:
代码也比较简单:
String body = et_body.getText().toString().trim();String sender = et_sender.getText().toString().trim();// 拿到内容解析者ContentResolver resolver = getContentResolver();Uri uri = Uri.parse("content://sms");ContentValues values = new ContentValues();values.put("address", sender);values.put("body", body);// 插入数据resolver.insert(uri, values);
测试结果:
利用提供者和解析这备份和插入短信还是比较简单的,有难度的在于备份联系人和插入联系人。这些将在下一个博客讲述。