Android四大组件—ContentProvider

1.ContentProvider是什么

       ContentProvider从字面上理解是内容提供器的意思,主要是用来实现程序与程序之间实现数据共享的功能,在Android中提供了一套完整的机制,它允许一个程序去访问另一个程序中的数据,同时还能保证数据的安全性。ContentProvider可以选择只对哪一部分的数据进行共享,不进行共享的数据其他的程序就无法访问,从而保证了程序中重要的信息不会泄漏。

       ContentProvider的用法一般有两种,一种是使用现有的内容提供器来读取相应程序中的数据,另一种是自定义自己的内容提供器并且选择好要共享的数据,提供给其他程序外部访问的接口。

2.访问其它程序中的数据

      在做开发的大多数时候用到ContentProvider并不是要共享自己程序中的数据,而是要去访问其它程序中的数据,最常用的功能就是读取手机中的联系人,短信等。Android系统中自带的电话簿、短信和媒体库等应用都为我们提供了类似的访问接口,这就使得第三方应用可以利用这些数据来实现更好的功能。

1)读取收件箱信息

//获取收件箱的哪些信息
public List<SMSContent> getSMS()
{
    String[] projection = new String[]
            {
             	"address",
                "body",
                "type",
                "date"
            };
    try
    {
     	Uri uri = Uri.parse("content://sms/");
        Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
        if (cursor.moveToFirst())
        {
            String address;
            String body;
            String date;
            String type;
            int phoneColumn = cursor.getColumnIndex("address");
            int bodyColumn = cursor.getColumnIndex("body");
            int dateColumn = cursor.getColumnIndex("date");
            int typeColumn = cursor.getColumnIndex("type");
            while(cursor.moveToNext())
            {
                SMSContent content = new SMSContent();
                address = cursor.getString(phoneColumn);
                body = cursor.getString(bodyColumn);
                date = cursor.getString(dateColumn);
                type = cursor.getString(typeColumn);
                content.setAddress(address);
                content.setBody(body);
                content.setDate(date);
                content.setType(type);
                SMSList.add(content);
            }
            cursor.close();
        }
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
    return SMSList;
}
       此时还要在AndroidManifest.xml中加入读取收件箱的权限
<uses-permission android:name="android.permission.READ_SMS"/>
       由上面的代码可以看出在得到收件箱URI地址后,通过ContentResolver的query(Uri uri,String[] projection,String selection,String[] selectionArgs,String sortOrder)的方法可以获取收件箱的全部信息。
—uri:ContentProvider需要返回的资源索引

projection:用于标识有哪些columns需要包含在返回数据中。例如:id号,地址,消息体,读取状态等

selection:做为查询符合条件的过滤参数

selectionArgs:过滤参数的数据

sortOrder:对于返回信息进行排列

       关于短信的columns分别为:

—_id//短消息序号 如100
thread_id//对话的序号 如100
address//发件人地址,手机号.如+8613811810000
person//发件人,返回一个数字就是联系人列表里的序号,陌生人为null
date//日期 long型。如1256539465022
protocol//协议 0 SMS_RPOTO, 1 MMS_PROTO
read//是否阅读 0未读, 1已读
status//状态 -1接收,0 完成, 64 未接收, 128 失败 
type//类型 1是接收到的,2是已发出
body//短消息内容
service_center //短信服务中心号码编号。如+8613800755500

       关于短信的URI和数据库表的字段分别为:

String SMS_URI_ALL = "content://sms/"; //全部
String SMS_URI_INBOX = "content://sms/inbox";//收件箱
String SMS_URI_SENT = "content://sms/sent"; //已发送
String SMS_URI_DRAFT = "content://sms/draft";//草稿箱     
String SMS_URI_OUTBOX = "content://sms/outbox";//发件箱    
String SMS_URI_FAILED = "content://sms/failed"; //发送失败
String SMS_URI_QUEUED = "content://sms/queued";//待发送列表

       query()的返回值是一个Cursor的对象,我们查询的数据都在Cursor的对象中,只需要将数据读出即可。

2)在收件箱中插入一条信息

       其实在收件箱中插入一条信息就是通过ContentResolver直接向数据库中写入信息

//插入一条信息到收件箱中
/*
    address:发件人地址(电话号码)
    type:信息类型(1是接收到的,2是已发出)
    date:日期
    body:/短消息内容
*/
public boolean insertSMS(String address, int type, long date, String body)
{
    try
    {
        Uri uri = Uri.parse("content://sms/");
        ContentResolver contentResolver = context.getContentResolver();
        ContentValues contentValues = new ContentValues();
        contentValues.put("address", address);
        contentValues.put("type", type);
        contentValues.put("date", date);
        contentValues.put("body", body);
        contentResolver.insert(uri, contentValues);
        return true;
    }
    catch (Exception e)
    {
        e.printStackTrace();
        return false;
     }
}
       此时还要在AndroidManifest.xml中加入写入收件箱的权限

<uses-permission android:name="android.permission.WRITE_SMS"/>
       由上面的代码可以看出在得到收件箱的URI地址后通过ContentResolver的insert(Uri url,ContentValues values)的方法可以往收件箱中插入一条信息,ContentValues是一种存储机制,使用put()方法和get()方法可以分别存入和取出数据,ContentValues只能存储基本类型的数据,像string、int之类的,不能存储对象这种东西。

3)读取手机通讯录

       跟读取收件箱相似,都是通过query()方法进行查询

//读取通讯录
public List<Contacts> getContact()
{
    try
    {
        Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
        //使用ContentResolver查找联系人数据
        Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
        //遍历查询结果,获取系统中所有的联系人
        if(cursor.moveToFirst())
        {
            String number;
            String displayName;
            int displayNameColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
            int numberColumn = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
            while (cursor.moveToNext())
            {
                contacts = new Contacts();
                //获取联系人姓名
                displayName = cursor.getString(displayNameColumn);
                //获取联系人手机号码
                number = cursor.getString(numberColumn);
                contacts.setName(displayName);
                contacts.setNumber(number);
                contactsList.add(contacts);
            }
        }
        cursor.close();
    }
    catch (Exception e)
    {
        e.printStackTrace();
    }
    return contactsList;
}
       此时还要在AndroidManifest.xml中加入读取联系人的权限
<uses-permission android:name="android.permission.READ_CONTACTS"/>
4)添加一个新的联系人

//插入联系人到通讯录
public boolean insertContact(String name, String number)
{
    try
    {
        Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
        Uri dataUri = Uri.parse("content://com.android.contacts/data");
        ContentResolver contentResolver = context.getContentResolver();
        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
        ContentProviderOperation op1 = ContentProviderOperation.newInsert(uri)
                    .withValue("account_name", null)
                    .build();
        operations.add(op1);
        //加入姓名
        ContentProviderOperation op2 = ContentProviderOperation.newInsert(dataUri)
                    .withValueBackReference("raw_contact_id", 0)
                    .withValue("mimetype", "vnd.android.cursor.item/name")
                    .withValue("data2", name)
                    .build();
        operations.add(op2);
        //加入电话号码
        ContentProviderOperation op3 = ContentProviderOperation.newInsert(dataUri)
                    .withValueBackReference("raw_contact_id", 0)
                    .withValue("mimetype", "vnd.android.cursor.item/phone_v2")
                    .withValue("data1", number)
                    .withValue("data2", "2")
                    .build();
        operations.add(op3);
        //将上述内容添加到电话号码中
        contentResolver.applyBatch("com.android.contacts", operations);
        return true;
    }
    catch(Exception e)
    {
        return false;
    }
}
       在新增联系人中使用到了ContentProviderOperation这样的类,Android系统中引入这个类是为了通ContentResolver的对应函数批量更新、插入、删除数据时更加方便,在开发文档中推荐使用ContentProviderOperation的原因为:

  • 所有的操作都在一个事务中执行,这样可以保证数据完整性 
  • 由于批量操作在一个事务中执行,只需要打开和关闭一个事务,比多次打开关闭多个事务性能要好些 
  • 使用批量操作和多次单个操作相比,减少了应用和content 
  • provider之间的上下文切换,这样也会提升应用的性能,并且减少占用CPU的时间,当然也会减少电量的消耗。
       创建ContentProviderOperation的对象需要使用ContentProviderOperation.Builder类,通过调用以下的静态方法获取Builder的对象(这里使用了著名的Builder设计模式)。

  • newInsert 创建一个用于执行插入操作的Builder
  • newUpdate 创建一个用于执行更新操作的Builder
  • newDelete 创建一个用于执行删除操作的Builder
       最后使用applyBatch()方法将ContentProviderOperation对象封装的内容添加到通讯录中

3.自定义ContentProvider

       自定义ContentProvider提供该应用程序的URI,将本程序的数据提供给第三方应用。这种情况在实际的开发中很少遇到,因为我们都不希望将自己的数据提供给其它APP使用,但是学会怎样使用自定义ContentProvider还是很有必要的。

1)首先需要新建一个类,继承自ContentProvider

package com.example.administrator.contentproviderdemo;


import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;

public class MyContentProvider extends ContentProvider
{

    //初始化
    public boolean onCreate()
    {
        return false;
    }

    //查询
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
    {
        return null;
    }

    //插入
    public Uri insert(Uri uri, ContentValues values)
    {
        return null;
    }

    //删除
    public int delete(Uri uri, String selection, String[] selectionArgs)
    {
        return 0;
    }

    //更新
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
    {
        return 0;
    }

    //得到MIME类型
    public String getType(Uri uri)
    {
        return null;
    }
}
       在使用子类继承 ContentProvider时,需要重写以上六个方法,接下来简单介绍一下这六个方法的作用
  • onCreate()—初始化ContentProvider的时候调用,只会调用一次。通常在这里完成数据库的创建和升级操作,只有当存在ContentResolver尝试访问我们程序中的数据时,内容提供器才会被初始化。
  • query()—查询数据时调用,uri用于确定查询哪张表,projection用于确定查询哪几列,selection和selectionArgs用于约束查询哪些行,sortOrder用于对查询到的结果进行排序。在查询成功后会将查询结果放在一个Cursor的对象中并返回。
  • insert()—插入数据时调用,uri用于确定插入数据到哪张表,values用于封装需要添加的数据。插入成功以后将会返回一个用于表示新纪录的URI。
  • update()—更新数据时调用,uri用于确定更新数据到哪张表,values用于封装需要更新的数据,selection和selectionArgs用于约束更新哪些行。更新成功以后会返回受影响的行数。
  • delete()—删除数据时调用,uri用于确定删除数据从哪张表,selection和selectionArgs用于约束删除哪些行。删除成功以后会返回被删除的行数。
  • getType()—返回MIME类型时调用,根据传入的内容URI来返回相应的MIME类型。

2)接下来在使用URI对哪些表中的哪些数据进行操作之前,还要对传入的URI进行匹配,由于ContentProvider可以提供我们想提供的数据,不想提供的数据可以不提供。

       要实现数据筛选就需要借助UriMatcher这个类实现URI匹配内容的功能,我们主要要使用这个类中的addURI()和match()方法。

addURI(String authority, String path, int code)—添加一个URI匹配

  • authority—权限匹配
  • path—路径匹配
  • code—自定义代码

match(Uri uri)—匹配URI的路径,返回一个能够匹配URI对象所对应的自定义代码,根据这个代码判断出调用方期望访问哪张表中的数据

  • uri—uri路径
//查询
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
{
    switch (uriMatcher.match(uri))
    {
        case TABLE1_DIR:

            break;
        case TABLE1_ITEM:

            break;
        case TABLE2_DIR:

            break;
        case TABLE2_ITEM:

            break;
        default:

            break;
    }
    return null;
}
       当query()方法被调用的时候,首先会通过UriMatcher的match()方法对传入的Uri进行匹配,如果发现UriMatcher中某个内容URI成功匹配了该Uri对象,则会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的数据是什么,再进行相关的操作。如果没有匹配成功则说明传入的Uri错误或者调用方期望访问的数据没有开放。
       上述代码只是以query()方法为例,其实insert()、update()、delete()这几个方法的实现是差不多的,在这几个方法中都有Uri这个参数,利用UriMatcher的match()方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的处理。
除此之外还有getType()这个方法,该方法是所有内容提供器都必须实现的一个方法,用于获取一个URI对象所对应的MIME类型
//得到MIME类型
public String getType(Uri uri)
{
    switch (uriMatcher.match(uri))
    {
        case TABLE1_DIR:
            return "vnd.android.cursor.dir/vnd.com.example.administrator.contentproviderdemo.table1";
        case TABLE1_ITEM:
            return "vnd.android.cursor.item/vnd.com.example.administrator.contentproviderdemo.table1";
        case TABLE2_DIR:
            return "vnd.android.cursor.dir/vnd.com.example.administrator.contentproviderdemo.table2";
        case TABLE2_ITEM:
            return "vnd.android.cursor.item/vnd.com.example.administrator.contentproviderdemo.table2";
        default:

            break;
    }
    return null;
}
一个内容URI对应的MIME字符串主要由三部分组成,Android对这三个部分做了如下格式规定:
  1. 必须以vnd开头
  2. 如果内容URI以路径结尾则后接android.cursor.dir/,如果内容URI以id结尾则后接android.cursor.item/
  3. 最后接上vnd.<authority><path>
最后还需要在AndroidManifest.xml文件中进行注册
<provider
    android:authorities="com.example.administrator.contentproviderdemo"
    android:name="com.example.administrator.contentproviderdemo.MyContentProvider"
    android:exported="true"/>
          注册的属性依次为用于匹配URI的权限名,类名和是否共享数据
       现在一个完整的自定义ContentProvider就创建完成,任何一个应用都可以通过ContentResolver来访问我们程序中的数据并且做相应的操作。但是其它应用也不是能够通过ContentResolver访问所有的数据,毕竟没有哪个开发者愿意把私密数据提供给其它应用使用,在这里还记得前面在访问数据之前要借助UriMatcher这个类的match()方法对传入的Uri进行匹配,只有通过匹配的Uri才能进行CRUD操作,正是由于这个良好的机制才能保证我们的私密数据泄漏,除非向UriMatcher中添加隐私数据的URI,所以隐私数据根本无法被外部程序访问到,安全问题也就不存在了。

4.通过ContentObserver监听ContentProvider的数据变化

ContentObserver—内容观察者,目的是观察特定Uri引起数据库变化而做出相应的处理。在使用ContentObserver的过程中需要自定一个类并且继承自ContentObserver并且重写onChange()方法

在这里我们要监听用户发短信的状态,此时需要知道短信的Uri分别为以下六种:

  1. content://sms/inbox—收件箱
  2. content://sms/sent—已发送
  3. content://sms/draft—草稿
  4. content://sms/outbox—发件箱(正在发送的信息)
  5. content://sms/failed—发送失败
  6. content://sms/queued—待发送列表(在开启飞行模式后,短信就在待发送列表中)
package com.example.administrator.contentproviderdemo;

import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import android.widget.Toast;

/**
 * Created by ChuPeng on 2017/1/4.
 */

public class MyContentObserver extends ContentObserver
{

    /**
     * Creates a content observer.
     *
     * @param handler The handler to run {@link #onChange} on, or null if none.
     */
    private Context context;
    public MyContentObserver(Handler handler, Context context)
    {
        super(handler);
        this.context = context;
    }

    /**
     * 当监听的Uri发生改变时,就会回调此方法
     *
     * @param selfChange 一般情况下为false,意义不是很大
     */
    public void onChange(boolean selfChange)
    {
        //查询发件箱中的短信
        Uri uri = Uri.parse("content://sms/outbox");
        Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
        if(cursor != null)
        {
            //对查询结果进行遍历
            while(cursor.moveToNext())
            {
                StringBuilder sb = new StringBuilder();
                //发送地址
                sb.append("address=").append(cursor.getString(cursor.getColumnIndex("address"))).append(";  ");
                //短信内容
                sb.append("body=").append(cursor.getString(cursor.getColumnIndex("body"))).append(";  ");
                //是否查看
                sb.append("read=").append(cursor.getString(cursor.getColumnIndex("read"))).append(";  ");
                //发送时间
                sb.append("time=").append(cursor.getString(cursor.getColumnIndex("date"))).append("\n");
                Log.d("sms", sb.toString());
            }
            //在使用完以后需要关闭
            cursor.close();
        }
        else
        {
            Toast.makeText(context, "查询失败", Toast.LENGTH_SHORT).show();
        }
    }
}
       在完成自定义的ContentObserver以后,想要进行监听还需要使用registerContentObserver()方法在相应的Activity中去进行注册
registerContentObserver(Uri uri, boolean notifyForDescendents, ContentObserver observer)

  • uri—需要监听的Uri(Uri已经被添加到UriMatcher)
  • notifyForDescendents—是布尔型的常量,为true时代表精确匹配,为false时代表模糊匹配(可以匹配派生出的Uri)
  • observer—ContentObserver的对象实例
       由于ContentObserver的生命周期不同步于Activity和Service等,因此,在不需要时,需要手动的调用unregisterContentObserver()方法去取消注册
unregisterContentObserver(ContentObserver observer)

  • observer—ContentObserver的对象实例
//监听ContentProvider的数据变化
MyContentObserver myContentObserver = new MyContentObserver(new Handler(), MainActivity.this);
getContentResolver().registerContentObserver(Uri.parse("content://sms"), true, myContentObserver);
       运行程序以后,在发送短信时可以在logcat中看到该条信息的内容,在实际开发中我们可以根据自己的需求在Service中开启监听,这样就可以在后台监听ContentProvider的数据变化。

5.总结

       在实际开发中使用ContentObserver主要有一下两种情况:

  • 需要频繁的监听数据库或者某个数据是否发生改变,这种情况下如果使用线程去操作就变得很耗时
  • 在用户不知道的情况下对数据库做一些改变,比如在后台悄悄发送短信等

       需要注意的一点是,如果我们使用ContentObserver监听一个自定义的ContentProvider,当自定义的ContentProvider数据发生改变时,却监听不到任何反应。这是因为在每个ContentProvider数据源发生改变时,如果想通知其监听对象就必须在对应的方法中(query()、insert()、update()和delete())调用this.getContentReslover().notifychange(uri, observer)方法,用来通知监听的ContentObserver,否则ContentObserver时不会监听到数据发生改变的。

notifyChange(Uri uri,ContentObserver observer)

uri—需要监听的Uri

observer—监听数据的ContentObserver对象,可以为null




       以上Demo的源代码地址:点击打开链接

       



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值