• Android 四大组件之四(ContentProvider)


    ContentProvider调用关系:

    ContentProvider(数据提供者)是应用程序之间共享数据的一种接口机制,是一种更为高级的数据共享方法。

    • ContentProvider可以指定需要共享的数据,而其他应用程序则可以在不知道数据来源、路径的情况下,对共享数据进行增删改查等操作。
    • 在Android系统中,许多Android系统内置的数据也是通过ContentProvider提供给用户使用,例如通讯录、音视频文件和图像文件等。

    相关概念介绍

    1)ContentProvider简介
           当应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。

    2)通用资源标识符(Uniform Resource Identifier)

          URI是一个用于标识某一互联网资源名称的字符串。 该种标识允许用户对任何(包括本地和互联网)的资源通过特定的协议进行交互操作。在ContentProvider机制中,使用ContentResolver对象通过URI定位ContentProvider提供的资源。
    ContentProvider使用的URI语法结构如下:

    content://<authority>/<data_path>/<id>  
    • content:// 是通用前缀,表示该UIR用于ContentProvider定位资源。
    • < authority > 是授权者名称,用来确定具体由哪一个ContentProvider提供资源。因此一般< authority >都由类的小写全称组成,以保证唯一性。
    • < data_path > 是数据路径,用来确定请求的是哪个数据集。如果ContentProvider近提供一个数据集,数据路径则可以省略;如果ContentProvider提供多个数据集,数据路径必须指明具体数据集。数据集的数据路径可以写成多段格式,例如people/girl和people/boy。
    • < id > 是数据编号,用来唯一确定数据集中的一条记录,匹配数据集中_ID字段的值。如果请求的数据不只一条,< id >可以省略。

    如请求整个people数据集的URI为:

    content://com.example.peopleprovider/people  

    而请求people数据集中第3条数据的URI则应写为:

    content://com.example.peopleprovider/people/3  

    以下是一些示例URI:

         content://media/internal/images  这个URI将返回设备上存储的所有图片
         content://contacts/people/  这个URI将返回设备上的所有联系人信息
         content://contacts/people/45 这个URI返回单个结果(联系人信息中ID为45的联系人记录)

      尽管这种查询字符串格式很常见,但是它看起来还是有点令人迷惑。为此,Android提供一系列的帮助类(在android.provider包下),里面包含了很多以类变量形式给出的查询字符串,这种方式更容易让我们理解。

    创建数据提供

    1. 创建一个类让其继承ContentProvider,并重载6个函数

      • onCreate()
        一般用来初始化底层数据集和建立数据连接等工作

      • getType()
        用来返回指定URI的MIME数据类型,若URI是单条数据,则返回的MIME数据类型以vnd.Android.cursor.item开头;若URI是多条数据,则返回的MIME数据类型以vnd.android.cursor.dir/开头。

      • insert()、delete()、update()、query()
        用于对数据集的增删改查操作。

    2. 声明CONTENT_URI,实现UriMatcher

    public static final String AUTHORITY = "com.example.peopleprovider";
    public static final String PATH_SINGLE = "people/#";
    public static final String PATH_MULTIPLE = "people";
    public static final String CONTENT_URI_STRING = "content://" + AUTHORITY + "/" + PATH_MULTIPLE;
    public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
    public static final int MULTIPLE_PEOPLE = 1;
    public static final int SINGLE_PEOPLE = 2;
    public static final UriMatcher uriMatcher;
    static{
          uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
          uriMatcher.addURI(AUTHORITY, PATH_SINGLE, SINGLE_PEOPLE );
          uriMatcher.addURI(AUTHORITY, PATH_MULTIPLE , MULTIPLE_PEOPLE );
    }

    其中UriMatcher类引用官方文档中的解释:

          Utility class to aid in matching URIs in content providers.

    可见UriMatcher本质上是一个文本过滤器,用在contentProvider中帮助我们过滤,分辨出查询者想要查询哪个数据表。
    UriMatcher的构造函数中,UriMatcher.NO_MATCH是URI无匹配时的返回代码,值为-1。 addURI() 方法用来添加新的匹配项,语法为:

    public void addURI(String authority, String path, int code)

    其中authority表示匹配的授权者名称,path表示数据路径(#代表任何数字),code表示返回代码。

    关于UriMatcher的使用

    switch(uriMatcher.match(uri)){
        case MULTIPLE_PEOPLE:
             //多条数据的处理
             break;
        case SINGLE_PEOPLE:
             //单条数据的处理
             break;
        default:
             throw new IllegalArgumentException("不支持的URI:" + uri);
    }

    3. 注册ContentProvider

    在AndroidManifest.xml文件中的 application节点下使用<provider>标签注册。示例:

    <provider
                android:authorities="com.example.peopleprovider"
                android:name=".Peopleprovider" />

    使用数据提供者

    每个Android组件都有一个ContentResolver对象,通过调用getContentResolver() 方法可得到ContentResolver对象。  
    

    准备工作:

    ContentResolver resolver = getContentResolver();
    String KEY_ID = "_id";
    String KEY_NAME = "name";
    String KEY_AGE = "age";
    String KEY_HEIGHT = "height";

    1. 添加操作

    通过insert()函数添加单条数据
    (returns : the URL of the newly created row.)

    ContentValues values = new ContentValues();
    values.put(KEY_NAME,
    "Tom"); values.put(KEY_AGE, 21); values.put(KEY_HEIGHT, 1.81f); Uri newUri = resolver.insert(CONTENT_URI, values);

    通过bulkInsert()函数添加多条数据
    (returns:the number of newly created rows.) 

    ContentValues[] arrayValues = new ContentValues[10];
    //实例化每一个ContentValues...
    int count = resolver.bulkInsert(CONTENT_URI, arrayValues );

    2. 删除操作

    指定ID删除单条数据

    Uri uri = Uri.parse(CONTENT_URI_STRING + "/" +"2");
    int result = resolver.delete(uri, null, null);

    通过selection语句删除多条数据

    String selection = KEY_ID + ">4";
    int result = resolver.delete(CONTENT_URI, selection, null);

    3. 更新操作

    ContentValues values = new ContentValues();
    values.put(KEY_NAME, "Tom");
    values.put(KEY_AGE, 21);
    values.put(KEY_HEIGHT, 1.81f);
    Uri rui = Uri.parse(CONTENT_URI_STRING + "/" + "7");
    int result = resolver.update(uri, values, null, null);

    4. 查询操作

    Uri uri = Uri.parse(CONTENT_URI_STRING + "/" + "2");
    Cursor cursor = resolver.query(uri, new String[]{KEY_ID, KEY_NAME, KEY_AGE, KEY_HEIGHT}, null, null, null);

    在URI中定义了需要查询数据的ID后,在query()函数中没有必要再加入其他的查询条件,如果要获取数据集全部数据,则可以直接使用CONTENT_URI且不加查询条件。
    在Android系统中,数据库查询结果的返回值并不是数据集合的完整拷贝,而是返回数据集的指针,这个指针就是Cursor类。ContentProvider的数据集类似数据库的数据表,其查询结果的返回值同样是数据集的指针:Cursor类。在提取Cursor数据中的数据前,推荐测试Cursor中的数据数量,避免在数据获取中产生异常。示例如下: 

    public people[] getPeople(Cursor cursor){
        int resultCounts = cursor.getCount();
        if(resultCounts == 0 || !cursor.moveToFirst()){
            return null;
        }
        People[] peoples = new People[resultCounts];
        for(int i=0; i<resultCounts; i++){
            peoples[i] = new People();
            peoples[i].ID = cursor.getInt(0);
            peoples[i].Name = cursor.getString(cursor.getColumnIndex(KEY_NAME));
            peoples[i].Age = cursor.getInt(cursor.getColumnIndex(KEY_AGE));
            peoples[i].Height= cursor.getFloat(cursor.getColumnIndex(KEY_HEIGHT));
            cursor.moveToNext();
        }
        return peoples;
    }

    其他:

    1.ContentUris类使用介绍

    ContentUris类用于操作Uri路径后面的ID部分,它有两个比较实用的方法:
    withAppendedId(uri, id)用于为路径加上ID部分:

    Uri uri = Uri.parse("content://com.ljq.provider.personprovider/person")
    
    Uri resultUri = ContentUris.withAppendedId(uri, 10); 
    //生成后的Uri为:content://com.ljq.provider.personprovider/person/10

    parseId(uri)方法用于从路径中获取ID部分:

    Uri uri = Uri.parse("content://com.ljq.provider.personprovider/person/10")
    long personid = ContentUris.parseId(uri);//获取的结果为:10

    2.监听ContentProvider中数据的变化

    如果ContentProvider的访问者需要知道ContentProvider中的数据发生变化,可以在ContentProvider发生数据变化时调用getContentResolver().notifyChange(uri,null)来通知注册在此URI上的访问者,例子如下:

    public class PersonContentProvider extends ContentProvider {
       public Uri insert(Uri uri, ContentValues values) {
          db.insert("person", "personid", values);
          getContext().getContentResolver().notifyChange(uri, null);
       }
    } 

    如果ContentProvider的访问者需要得到数据变化通知,必须使用ContentObserver对数据(数据采用uri描述)进行监听,当监听到数据变化通知时,系统就会调用ContentObserver的onChange()方法:

    getContentResolver().registerContentObserver(Uri.parse("content://com.ljq.providers.personprovider/person"),
           true, new PersonObserver(new Handler()));
    public class PersonObserver extends ContentObserver{
       public PersonObserver(Handler handler) {
          super(handler);
       }
       public void onChange(boolean selfChange) {
          //此处可以进行相应的业务处理
       }
    } 

    3.权限设置与线程同步

    1.     可以在代码中通过setReadPermission()和setWritePermission()两个方法来设置ContentProvider的操作权限,也可以在配置文件中通过android:readPermission和android:writePermission属性来控制。

    2.     因为ContentProvider可能被不同的进程和线程调用,所以里面的方法必须是线程安全的。

    Content Provider的权限的管理很复杂,所以需要慢慢的说。

    一个Provider里面可能有私有数据,也有公有数据。也就是说,有可能有些数据可以公开,有些不能公开。并且,有些数据可以让别人修改,有些不能让别人修改。

    围绕上诉的可能情况,Provider就需要设置读权限(android:readPermission),和写权限(android:writePermission),或者干脆都设置(android:permission)。因为一个Provider可能被多个程序共同调用,那么这个Provider的数据,就需要做同步处理,因此需要设置android:multiprocess="true"

    那么怎么控制哪些数据是可以操作的,哪些又是不能操作的呢?Provider是通过URI来识别需要操作的数据是什么,因此数据的限制就需要体现在对URI的控制上。

    path-permission,控制访问在这个路径下的数据的权限,如:

    <path-permission android:pathPrefix="/users" android:permission="lichie.provider.permission"/>
    意思就是,访问“/users”这个路径下的数据,必须要有"lichie.provider.permission"的权限。
    值得注意的是:如果provider没有设置权限,只设置了path-permission的权限,那么在android 2.3.3版本中,path-permission设置的权限,是不会生效的。
    <provider android:name=".PackageProvider" 
              android:authorities="com.ygomi.packageprovider"             
              android:multiprocess="true"             
              android:readPermission="com.ygomi.packageprovider.permission.read">         
         <path-permission android:pathPattern="/apks/.*"              
                     android:permission="com.ygomi.packageprovider.permission.application.read"/>        
    </provider>

    这段代码的path-permission是有效的,但是下面这段代码是无效的。

    <provider android:name=".PackageProvider" 
              android:authorities="com.ygomi.packageprovider"            
              android:multiprocess="true" >         
         <path-permission android:pathPattern="/apks/.*"              
                          android:permission="com.ygomi.packageprovider.permission.application.read"/>       
    </provider>

    android:grantUriPermissions,管理哪个范围的数据权限需要处理。这个属性其实不用显示的设置,因为如果设置了android:readPermission, android:writePermission ,android:permission中的任意一个android:grantUriPermissions就默认是true了;如果设置了grant-uri-permission,那么android:grantUriPermissions默认就是false;如果都设置了,那么android:grantUriPermissions也是false。

    grant-uri-permission这个东西是非常难理解了。文档上虽然说是在没有权限的程序,需要访问Provider的时候,用于绕过权限控制的。但是这个东西第一次使用,还是非常的难。下面来细说一下。
     
    首先,要建立一个观念,没有权限,就一定不能访问,否则就有安全隐患。我们来举个例子:
    应用程序(Application)A有一个Provider来提供一些数据给其他程序访问,但是他需要设置一个权限控制,所以在应用程序A的配置文件中,就有了下面一段代码:
    <provider android:name=".MyProvider" 
              android:authorities="mytest.testProvider"             
              android:readPermission="lichie.provider.permission"            
              android:multiprocess="true">            
        <grant-uri-permission android:pathPrefix="/user/" />        
    </provider>
    意思就是,除了应用程序A外,其他所有的程序,都必须具有"lichie.provider.permission"的权限,才能访问Provider的数据。但是允许大家在没有权限的时候,通过"/user/"访问。等等,等等,没有权限也可以用“/user/”访问,那么要权限来有啥子用啦?
     
    精彩的地方来了,其实grant-uri-permission的作用是使,调用Provider的程序(我们这里叫应用程序B)可以没有权限,但是调用应用程序B的程序(我们这里叫应用程序C),必须要有权限。
     
    所以,在应用程序C的配置文件中,需要有以下代码:
    <uses-permission android:name="lichie.provider.permission" />

    应用程序C中,调用程序B的代码如下:

    Intent intent = new Intent();        
    intent.addCategory("lichie.category.one");
    intent.setAction("lichie.action.one");
    intent.setData(Uri.parse("content://mytest.testProvider/ddd/"));
    intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

    应用程序B的配置文件中,需要有一段如下:

    <intent-filter> 
    <action android:name="lichie.action.one" />
    <category android:name="lichie.category.one" />
    <data android:scheme="content" android:pathPrefix="mytest.testProvider"/>
    <category android:name="android.intent.category.DEFAULT" />
    </
    intent-filter>

    应用程序B中,调用Provider的代码如下:

    <provider android:name=".PackageProvider" 
    android:authorities
    ="com.ygomi.packageprovider"
    android:multiprocess
    ="true"
    android:readPermission
    ="com.ygomi.packageprovider.permission.read">
    <path-permission android:pathPattern="/apks/.*"
    android:permission
    ="com.ygomi.packageprovider.permission.application.read"/>
    </provider>

    这样,表面上看,应用程序B没有访问的权限,但是因为调用它的程序C,具有了访问权限,所以程序B也能访问Provider了。

  • 相关阅读:
    记一次file_get_contents报failed to open stream: HTTP request failed! HTTP/1.1 400 Bad Request的错
    记一次centos7下配置服务器的过程
    locate: 无法执行 stat () `/var/lib/mlocate/mlocate.db': 没有那个文件或目录
    VM12虚拟机Centos7配置动态IP的网络设置
    记录下防御SSH爆破攻击的经验(CentOS7.3)
    第6次实践作业 17组
    第5次实践作业
    第3次实践作业
    第2次实践作业
    第1次实践作业
  • 原文地址:https://www.cnblogs.com/blosaa/p/6242114.html
Copyright © 2020-2023  润新知