内容提供者ContentProvider,是Android 的四大组件之一。内容提供者是应用程序之间共享数据的接口。应用程序创建的数据库,默认情况下是私有的,别的应用程序访问不到数据,如果想把数据对外提供,就要用到内容提供。ContentProvider屏蔽了数据存储的细节,内部实现对用户完全透明, 用户只需要关心操作数据的uri就可以了,ContentProvider可以实现不同app之间共享。 Sql也有增删改查的方法,但是sql只能查询本应用下的数据库。 而ContentProvider 还可以去增删改查本地文件/xml文件的读取等。Android 系统将这种机制应用到方方面面,比如:联系人(通讯录应用程序)Provider 专为不同应用程序提供联系人数据;短信(短信应用程序)Provider 专为不同应用程序提供系统短信信息。当应用继承ContentProvider 类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用SharedPreferences 共享数据,需要使用SharedPreferences API 读写数据。而使用ContentProvider 共享数据的好处是统一了数据访问方式。总之,内容提供者管理了对结构化数据最常见的就是数据库中数据的访问,操作内容提供者是不同进程之间以数据库数据形式交互数据的标准方式。
自定义的内容提供者包括内容提供者和访问者两个部分。
内容提供者,拥有自己的数据库,将数据库暴露出来供访问者修改。ContenProvider的编写基本步骤:
1. 写一个类继承 ContentProvider;
2. 重写一系列的方法,包括数据库操作的空实现;
3. 在内容提供者代码内部定义UriMatcher -用于判断uri是否匹配
static UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { mUriMatcher.addURI("在清单文件里面定义的authorities", "自定义匹配字符串", 成功返回的标识);
}
4. 在增删改查执行的时候判断uri是否合法,在内容提供者内部实现对数据库的增删改查;
5. 清单文件的下面声明provider,这里需要指定主机名,也就是对外提供的Uri,当访问者在内容解析者中传入同一个uri时,才可以访问到数据库;
<provider
android:name="com.itheima.db.BankDBBackdoor"
android:authorities="自定义主机名" >
</provider>
访问者,存在于另外一个工程中,可以对内提供者的数据库进行操作。
1. 创建内容提供者解析器
ContentResolver resolver = 上下文.getContentResolver();
2. 定义要访问的Uri路径
Uri uri = Uri.parse(“content://自定义主机名/自定义匹配字符串”) // “content://”是标准写法
3. 利用内容提供者解析器进行增删改查,实现对数据库的操作
内容提供者Uri 的书写模板: content:// 主机名authority/path/id。具体的书写规范如下所示:
1. “content://” 这是个固定写法,用来说明一个ContentProvider 控制这些数据。
2. 主机名或授权Authority:它定义了是哪个ContentProvider 提供这些数据。
3. path:路径,URI 下的某一个Item。
4. ID:通常定义Uri 时使用”#”号占位符代替, 使用时替换成对应的数字。#表示数据id,#代表任意数字,*用来来匹配任意文本
使用内容提供者操作系统短信和操作系统联系人是我们开发中经常遇到的需求,而自定义内容提供者对外提供数据反而使用的场景并不多,除非我们开发的短信或者联系人应用。一个小细节是,由于读取和插入系统短信数据库都涉及到可能侵犯用户隐私,因此创建的工程必须添加相关的权限。下面就分别讲解一下使用内容提供者操作系统数据库和自定义内容提供者。
用内容提供者操作系统短信只需要关注的到系统短信数据库的一张表,最长用的数据有body,date,type,address,各自的含义也较为直观。body表示短信的内容,date表示发送短信或收到短信的时间,type表示是受到短信还是发送短信,address表示收到的短信来自于哪个手机号。读取和插入系统短信数据库需要添加如下权限:
<uses-permission android:name=”android.permission.READ_SMS”/>
<uses-permission android:name=”android.permission.WRITE_SMS”/>
使用内容观察者操作系统联系人时需要关注三张张数据表,rawcontact表 ,data表和 mimetype表。其中raw_contacts 表存放的是联系人id 信息,一个联系人就是表中的一行记录。ontact_id在表单里应该是唯一的存在, 所以在插入的时候需要先查询最后一条id是多少,然后在此基础上加一。data 表中存放的是raw_contacts中的每一条id 对应的具体信息,一个联系人可能有电话、邮件、姓名等多条信息,每一条信息在该表中都是一行记录。为了区分不同信息的类型,因此还有一个mimetypes 表,该表存储的是常量数据,不同类型的信息由mimetype_id 来标识。现在很多App 都可以对系统联系人进行操作,这样就可以直接将号码添加到系统联系人中,可以关联/备份/恢复系统联系人。读取联系人信息的基本步骤是,首先查询rawcontact表,获取联系人的contactid,在rawcontact表中并不是每一个contact_id对应一条信息,而是一个contact_id对应多条信息,这样可以存储更多的信息。其次查询根据contact_id查询data表,获取联系人的数据 data1、mimetype,前者存储相关数据,后者存储该数据对应得数据。最后根据mimetype类型确定数据类型。修改联系人的操作是往raw_contacts 表中插入一个id,值为n+1,作为一条新的记录,然后往data1 表中插入具体的数据,其中id 必须为n+1。读取和插入系统联系人数据库需要添加如下权限:
<uses-permission android:readPermission=”android.permission.READ_CONTACTS”
<uses-permission android:writePermission=”android.permission.WRITE_CONTACTS”
那么怎么才能获取短信和联系人的Uri呢。这时就需要看源码了。打开Android 系统源码,其中TelephonyProvider 就是短信的内容提供者文件。打开TelephonyProvider 下的src 文件,查看java 文件,其中的SmsProvider.java 即短信息内容提供者逻辑代码。通过查找系统源码,可以确定短信息内容提供者的Uri 应该为:”content://sms”。用同样的方法,可以查到联系人内容提供者的Uri路径。打开Android 源码,查看packagesproviders路径下的文件,其中ContactsProvider 就是联系人的内容提供者。打开ContactsProvider2.java文件,查看此内容提供者的uri 路径。根据源码,确定内容提供者的Uri 信息为:操作raw_contacts 表的Uri:content://com.android.contacts/raw_contacts。操作data 表的Uri:content://com.android.contacts/data。其实,平常写代码时不必要这么复杂,直接把Uri路径拿来就可以用了。另外要注意的是,由于联系人数据库使用了视图,所以操作数据库表时,看到的表字段名称和真实操作的有所不同。比如:data 表在查询的时候没有mimetype_id 字段,取代的是mimetype 字段。
再来讲一讲内容观察者。内容提供者相当于一个监听。观察数据库内容是否发生改变,如果改变,通知观察者。内容观察者ContentObserver,目的是观察(捕捉)特定Uri 引起的数据库的变化,继而做一些相应的处理,它类似于数据库技术中的触发器(Trigger),当ContentObserver 所观察的Uri 发生变化时,便会触发它。触发器分为表触发器、行触发器,相应的ContentObserver 也分为“表ContentObserver”、“行ContentObserver”,当然这是与它所监听的Uri MIME Type 有关的。
内容观察者的使用步骤:
1. 在内容提供者类增加通知方法
getContext().getContentResolver().notifyChange(uri, null);
2. 在观察者类注册观察
//定义观察的uri ,和内容提供者的Uri一直
Uri uri = Uri.parse(“content://…”);
//注册观察者
getContentResolver().registerContentObserver(uri, true, ContentObserver);
最后通过案例来演示今天的知识点。
案例一: 内容提供者操作短信
java代码
/* * 添加权限 * <uses-permission android:name="android.permission.READ_SMS"/> * <uses-permission android:name="android.permission.WRITE_SMS"/> */ import android.app.Activity; import android.content.ContentResolver; import android.content.ContentValues; import android.net.Uri; import android.os.Bundle; import android.view.View; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } /**利用内存提供者添加短信*/ public void add(View view) { Uri uri = Uri.parse("content://sms"); ContentResolver resolver = getContentResolver(); ContentValues values = new ContentValues(); values.put("address", "00000"); values.put("date", System.currentTimeMillis()); values.put("type", 1); // 表示收短信还是发短信 values.put("body", "看到短信表示利用内容提供者添加短信成功"); resolver.insert(uri, values); } /**利用内存提供者删除短信*/ public void delete(View view) { Uri uri = Uri.parse("content://sms"); ContentResolver resolver = getContentResolver(); resolver.delete(uri, "address = ?", new String[] {"00000"}); } }
布局文件中只定义了两个按钮
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="add" android:text="添加短信" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="delete" android:text="删除短信" /> </LinearLayout>
效果展示:
添加短息:
删除短信:
案例二: 内容提供者操作联系人
JAVA代码,主程序:
import java.util.List; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.ListView; import android.widget.TextView; import com.example.contact.domain.ContactInfo; import com.example.contact.utils.ContactUtils; //添加权限<uses-permission android:name="android.permission.READ_CONTACTS"/> public class MainActivity extends Activity { private static final String TAG = "MainActivity"; private ListView listview; private List<ContactInfo> contactlist; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listview = (ListView) findViewById(R.id.listview); contactlist = ContactUtils.getContact(this); Log.i(TAG, "info" + contactlist.size()); for (ContactInfo info : contactlist) { Log.i(TAG, "info" + info.toString()); } listview.setAdapter(new ContactAdapter()); } public class ContactAdapter extends BaseAdapter { private ContactInfo contactInfo; @Override public int getCount() { return contactlist.size(); } @Override public Object getItem(int position) { return null; } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { viewHolder holder; if (convertView == null) { holder = new viewHolder(); convertView = View.inflate(MainActivity.this, R.layout.item_contact, null); holder.tv_text = (TextView) convertView .findViewById(R.id.tv_text); convertView.setTag(holder); } else { holder = (viewHolder) convertView.getTag(); } contactInfo = contactlist.get(position); holder.tv_text.setText(contactInfo.toString()); return convertView; } } public class viewHolder { TextView tv_text; } }
联系人工具类:
public class ContactInfo { private String name; private String phone; private String email; private String qq; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public String getQq() { return qq; } public void setQq(String qq) { this.qq = qq; } @Override public String toString() { return "ContactInfo [name=" + name + ", phone=" + phone + ", email=" + email + ", qq=" + qq + "]"; } }
获取系统联系人:
import java.util.ArrayList; import java.util.List; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import com.example.contact.domain.ContactInfo; public class ContactUtils { public static List<ContactInfo> getContact(Context context) { List<ContactInfo> list = new ArrayList<ContactInfo>(); ContentResolver resolver = context.getContentResolver(); Uri uri = Uri.parse("content://com.android.contacts/raw_contacts"); Uri datauri = Uri.parse("content://com.android.contacts/data"); Cursor cursor = resolver.query(uri, new String[] { "contact_id" }, null, null, null); while (cursor.moveToNext()) { String id = cursor.getString(0); System.out.println("00"+id); if (id != null) { ContactInfo info = new ContactInfo(); Cursor datacursor = resolver.query(datauri, new String[] { "data1", "mimetype" }, "raw_contact_id = ?", new String[] { id }, null); while (datacursor.moveToNext()) { String data1 = datacursor.getString(0); String mimetype = datacursor.getString(1); if ("vnd.android.cursor.item/name".equals(mimetype)) { info.setName(data1); } else if ("vnd.android.cursor.item/im".equals(mimetype)) { info.setQq(data1); } else if ("vnd.android.cursor.item/email_v2" .equals(mimetype)) { info.setEmail(data1); } else if ("vnd.android.cursor.item/phone_v2" .equals(mimetype)) { info.setPhone(data1); } } datacursor.close(); list.add(info); } } cursor.close(); return list; } }
布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" > <ListView android:id="@+id/listview" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
成果展示:
案例三: 内容提供者修改数据库。通过上述知识我们知道当需要对另外一个应用程序的数据库操作的时候可以用到内容提供者,那到底内容提供者内部是如何工作的呢?在这里给出一个案例来说明内容提供者的工作机制。首先,创建一个工程,在这个工程中新建一个数据库,单独写一个类继承ContentProvider,我们称之为后门程序,重写其中一系列的方法。其中包括对数据增删改查。但此处的数据库操作方法是空实现,当我们在另一工程中为内容解析者指定同一Uri路径时。调用内容解析者的增删改查方法时,会自动对该数据哭库操作。同时,还可以在后门程序中设置内容监听者,这样可以随时观察到数据的变化。
内容提供者的主程序,不用操作任何逻辑:
import android.os.Bundle; import android.app.Activity; import android.view.Menu; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }
内容提供者中创建数据库:
import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; public class MyDBOpenhelper extends SQLiteOpenHelper { public MyDBOpenhelper(Context context) { super(context, "test.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL("create table account (_id integer primary key autoincrement,name varchar(20),number varchar(20))"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }
写一个类ContentProvider:
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; public class BackDoor extends ContentProvider { private static final int SUCCESS = 1; /** 判断Uri规则 */ static UriMatcher mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); static { mUriMatcher.addURI("com.exmple.text", "account", SUCCESS); //uri规则可自己定义,但一定和清单文件一直 } @Override public boolean onCreate() { return false; } /** 增删改查为空实现 */ @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { int code = mUriMatcher.match(uri); // 判断Uri是否合法 if (code == SUCCESS) { System.out.println("查询数据"); MyDBOpenhelper helper = new MyDBOpenhelper(getContext()); SQLiteDatabase db = helper.getReadableDatabase(); return db.query("account", projection, selection, selectionArgs, null, null, sortOrder); } else { throw new IllegalArgumentException("路径不正确"); } } @Override public String getType(Uri uri) { return null; } @Override public Uri insert(Uri uri, ContentValues values) { int code = mUriMatcher.match(uri); if (code == SUCCESS) { System.out.println("添加数据"); MyDBOpenhelper helper = new MyDBOpenhelper(getContext()); SQLiteDatabase db = helper.getWritableDatabase(); db.insert("account", null, values); getContext().getContentResolver().notifyChange(uri, null); // 内容观察者检测数据库是否更改 } else { throw new IllegalArgumentException("路径不正确"); } return null; } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int code = mUriMatcher.match(uri); if (code == SUCCESS) { System.out.println("删除数据"); MyDBOpenhelper helper = new MyDBOpenhelper(getContext()); SQLiteDatabase db = helper.getWritableDatabase(); db.delete("account", selection, selectionArgs); getContext().getContentResolver().notifyChange(uri, null); // 内容观察者检测数据库是否更改 } else { throw new IllegalArgumentException("路径不正确"); } return 0; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int code = mUriMatcher.match(uri); if (code == SUCCESS) { System.out.println("更新数据"); MyDBOpenhelper helper = new MyDBOpenhelper(getContext()); SQLiteDatabase db = helper.getWritableDatabase(); db.update("account", values, selection, selectionArgs); getContext().getContentResolver().notifyChange(uri, null); // 内容观察者检测数据库是否更改 } else { throw new IllegalArgumentException("路径不正确"); } return 0; } }
配置文件中添加provider节点:
<!-- 注册内容提供者数据 --> <provider android:name="com.example.provider.BackDoor" android:authorities="com.exmple.text" > </provider>
调用者的主程序:
import android.app.Activity; import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.view.View; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } /** * 利用后门程序 添加一条数据 */ public void insert(View view) { ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://com.exmple.text/account"); ContentValues values = new ContentValues(); values.put("name", "zhangsan"); values.put("number", 10000); resolver.insert(uri, values); } /** * 利用后门程序 删除一条数据 */ public void delete(View view) { ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://com.exmple.text/account"); resolver.delete(uri, "name=?", new String[] { "zhangsan" }); } /** * 利用后门程序 修改数据 */ public void update(View view) { ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://com.exmple.text/account"); ContentValues values = new ContentValues(); values.put("number", 20000); resolver.update(uri, values, "name=?", new String[] { "zhangsan" }); } /** * 利用后门程序 查询数据 */ public void query(View view) { ContentResolver resolver = getContentResolver(); Uri uri = Uri.parse("content://com.exmple.text/account"); Cursor cursor = resolver.query(uri, new String[] { "name", "number" }, null, null, null); while (cursor.moveToNext()) { String name = cursor.getString(0); float number = cursor.getFloat(1); System.out.println("name:" + name + "----" + "number:" + number); } cursor.close(); } }
调用者的布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".MainActivity" > <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="insert" android:text="增" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="delete" android:text="删" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="update" android:text="改" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:onClick="query" android:text="查" /> </LinearLayout>
运行结果:
最新评论