第9章 使用ContentProvider实现数据共享

本章要点
  • 理解ContentProvider的功能与意义
  • ContentProvider类的作用和常用方法
  • Uri 对 ContentProvider的作用
  • 理解ContentProvider与ContentResolver的关系
  • 实现自己的ContentProvider
  • 配置ContentProvider
  • 使用ContentResolver操作数据
  • 操作系统ContentProvider提供的数据
  • 监听ContentProvider的数据改变
  • ContentObserver类的作用和常用方法
  • 监听系统ContentProvider的数据改变

在Android应用开发中,不同的应用有时需要共享数据。例如,一个短信接收应用可能需要将收到的陌生短信发件人添加到联系人管理应用中。为实现这种跨应用的数据共享,虽然可以直接操作另一个应用程序的数据(如SharedPreferences、文件或数据库),但这种方式不仅操作复杂,还存在严重的安全风险。因此,Android不推荐这种方法,而是建议使用ContentProvider。

ContentProvider是Android提供的用于应用程序之间数据交换的标准API。当一个应用程序希望将其数据暴露给其他程序使用时,可以通过实现ContentProvider来提供这些数据接口。其他应用程序则可以通过ContentResolver来访问和操作这些通过ContentProvider暴露的数据。

作为Android应用的四大组件之一,ContentProvider与Activity、Service和BroadcastReceiver类似,必须在AndroidManifest.xml文件中进行配置。一旦某个应用通过ContentProvider暴露了数据操作接口,其他应用便可以通过该接口访问和操作该应用的数据,无论该应用是否正在运行。这种操作包括数据的增、删、改、查等。

9.1 数据共享标准:ContentProvider

ContentProvider 是用于不同应用程序之间数据交换的标准API。它通过某种形式的Uri对外提供数据,允许其他应用程序访问或修改这些数据。其他应用程序可以使用ContentResolver根据指定的Uri来访问和操作这些数据。

提示:

对于初学者来说,理解ContentProvider和ContentResolver这两个核心API的作用可能需要一定的时间。这里有一个简单的类比:可以将ContentProvider视为Android系统内部的“网站”,这个“网站”通过固定的Uri对外提供服务;而ContentResolver则类似于Android系统内部的“HttpClient”,它可以向指定的Uri发送“请求”(实际上是调用ContentResolver的方法)。这些请求最终由ContentProvider处理,从而实现对“网站”(即ContentProvider)内部数据的操作。

9.1.1 ContentProvider 简介

如果将ContentProvider比作一个“网站”,那么如何对外提供数据呢?是否需要像Java Web开发一样编写JSP、Servlet之类的代码呢?答案是否定的。这种做法过于复杂,毕竟ContentProvider只是提供数据的访问接口,而不是像网站那样提供完整的页面。

如果把ContentProvider当作一个“网站”,如何完整地开发一个ContentProvider呢?步骤其实很简单,如下所示:

  1. 定义自己的ContentProvider类:该类需要继承Android提供的ContentProvider基类。
  2. 向Android系统注册这个“网站”:在AndroidManifest.xml文件中注册这个ContentProvider,就像注册Activity一样。在注册ContentProvider时,需要为它绑定一个Uri。

<application ... />元素下添加如下子元素来注册ContentProvider:

<provider 
    android:name=".DictProvider"
    android:authorities="org.crazyit.providers.dictprovider"
    android:exported="true"/>

提示:
虽然我们可以把ContentProvider看作是一个“网站”,但是Android官方文档并没有这样的描述。Android要求在注册ContentProvider时指定authorities属性,该属性的值类似于网站的域名。但需要提醒读者:在面试时千万不要使用这种比喻,因为面试官可能因为对技术的理解不够深而不认同这种说法,从而导致面试失败。

通过配置文件注册了DictProvider之后,其他应用程序就可以通过该Uri访问DictProvider暴露的数据。

对于ContentProvider来说,它主要用于执行CRUD操作,因此DictProvider除了需要继承ContentProvider之外,还需要提供以下几个方法:

  • boolean onCreate():该方法在ContentProvider创建后被调用。当其他应用程序第一次访问ContentProvider时,该ContentProvider会被创建,并立即回调onCreate()方法。
  • Uri insert(Uri uri, ContentValues values):根据指定的Uri插入对应的ContentValues数据。
  • int delete(Uri uri, String selection, String[] selectionArgs):根据Uri删除符合selection条件的所有记录。
  • int update(Uri uri, ContentValues values, String selection, String[] selectionArgs):根据Uri修改符合selection条件的所有记录。
  • Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder):根据Uri查询符合selection条件的所有记录,projection是一个列名列表,表示只选择指定的数据列。
  • String getType(Uri uri):该方法用于返回当前Uri代表的数据的MIME类型。如果该Uri对应的数据可能包括多条记录,MIME类型字符串应该以vnd.android.cursor.dir/开头;如果该Uri对应的数据只包含一条记录,MIME类型字符串应该以vnd.android.cursor.item/开头。

通过上面的介绍,不难发现,对于ContentProvider而言,Uri是一个非常重要的概念。接下来将详细介绍Uri的相关知识。

9.1.2 Uri 简介

在介绍Android系统的Uri之前,先来看一个最常用的互联网URL。例如,要访问疯狂Java联盟的某个页面,可以在浏览器中输入如下URL:

http://www.crazyit.org/index.php

对于这个URL,可以分为以下三个部分:

  1. http://:URL的协议部分,当通过HTTP协议来访问网站时,这个部分是固定的。
  2. www.crazyit.org:域名部分,指定要访问的网站,这个部分也是固定的。
  3. index.php:网站资源部分,当访问者需要访问不同资源时,这个部分是动态变化的。

类似地,ContentProvider要求的Uri也可以分为三个部分。例如,下面是一个ContentProvider的Uri:

content://org.crazyit.providers.dictprovider/words

这个Uri可以分为以下三个部分:

  1. content://:这是Android的ContentProvider规定的协议部分,就像互联网URL的协议部分是http://一样。暴露ContentProvider和访问ContentProvider的协议默认是content://
  2. org.crazyit.providers.dictprovider:这是ContentProvider的authorities部分,用于识别特定的ContentProvider。访问指定的ContentProvider时,这个部分是固定的。
  3. words:资源部分(或者说数据部分),表示具体要访问的资源。当访问者需要访问不同的资源时,这个部分是动态变化的。

需要指出的是,Android的Uri表达的功能更丰富,它还支持以下形式的Uri:

content://org.crazyit.providers.dictprovider/word/2

这个Uri表示要访问的资源为word/2,意味着访问word数据中ID为2的记录。

还有如下形式的Uri:

content://org.crazyit.providers.dictprovider/word/2/word

这个Uri表示要访问的资源为word/2/word,意味着访问word数据中ID为2的记录的word字段。

如果想访问全部数据,可以使用以下形式的Uri:

content://org.crazyit.providers.dictprovider/words

提示:

如果读者有RESTful的开发经验,那么对ContentProvider的Uri会感到熟悉,因为这些Uri基本遵循了RESTful风格。

虽然大多数使用ContentProvider操作的数据都来自数据库,但这些数据有时也可以来自文件、XML或网络等其他存储方式。此时,支持的Uri形式也可以是:

content://org.crazyit.providers.dictprovider/word/detail/

这个Uri表示操作word节点下的detail节点。

为了将一个字符串转换成Uri,Uri工具类提供了parse()静态方法。例如,以下代码可以将字符串转换为Uri:

Uri uri = Uri.parse("content://org.crazyit.providers.dictprovider/word/2");

9.1.3 使用ContentResolver操作数据

前面已经提到,ContentProvider相当于一个“网站”,它的作用是暴露可供操作的数据;其他应用程序则通过ContentResolver来操作ContentProvider所暴露的数据。ContentResolver可以看作是“HttpClient”。

Context类提供了以下方法来获取ContentResolver对象:

  • getContentResolver():获取该应用默认的ContentResolver。

一旦在程序中获得了ContentResolver对象,接下来就可以调用ContentResolver的以下方法来操作数据:

  • insert(Uri uri, ContentValues values):向Uri对应的ContentProvider中插入values对应的数据。
  • delete(Uri uri, String where, String[] selectionArgs):删除Uri对应的ContentProvider中符合where条件的数据。
  • update(Uri uri, ContentValues values, String where, String[] selectionArgs):更新Uri对应的ContentProvider中符合where条件的数据。
  • query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder):查询Uri对应的ContentProvider中符合where条件的数据。

通常,ContentProvider是单实例模式的,当多个应用程序通过ContentResolver来操作ContentProvider提供的数据时,ContentResolver调用的数据操作将会委托给同一个ContentProvider进行处理。

9.2 开发ContentProvider

对于许多初学者而言,理解ContentProvider如何暴露数据是一个难点。ContentProvider总是需要与ContentResolver结合使用,前者负责暴露数据,后者负责操作这些数据。因此,学习ContentProvider时需要同时了解ContentResolver的工作方式。

9.2.1 ContentProvider与ContentResolver的关系

从ContentResolver、ContentProvider和Uri的关系来看,无论是ContentResolver还是ContentProvider,它们所提供的CRUD方法的第一个参数都是Uri。也就是说,Uri是ContentResolver和ContentProvider进行数据交换的标识。

ContentResolver对指定的Uri执行CRUD等数据操作,但Uri并不是真正的数据存储位置。因此,这些CRUD操作会委托给该Uri对应的ContentProvider来实现。

通常情况下,如果A应用通过ContentResolver执行CRUD操作,这些操作都需要指定Uri参数。Android系统根据该Uri找到对应的ContentProvider(该ContentProvider通常属于B应用),ContentProvider则负责实现CRUD方法,完成对底层数据的增、删、改、查操作。这样,A应用就可以访问和修改B应用的数据。

ContentResolver、Uri和ContentProvider三者之间的关系如下图9.1所示:

A应用 (ContentResolver) ———通过Uri执行CRUD操作———> ContentProvider (B应用)
         (间接调用ContentProvider的CRUD方法)                        (对数据进行操作)
图9.1 ContentResolver、Uri与ContentProvider的关系

从图9.1可以看出,ContentResolver通过指定的Uri,可以实现“间接调用”ContentProvider的CRUD方法。

  • 当A应用调用ContentResolver的insert()方法时,实际上相当于调用了该Uri对应的ContentProvider(属于B应用)的insert()方法。
  • 当A应用调用ContentResolver的update()方法时,实际上相当于调用了该Uri对应的ContentProvider(属于B应用)的update()方法。
  • 当A应用调用ContentResolver的delete()方法时,实际上相当于调用了该Uri对应的ContentProvider(属于B应用)的delete()方法。
  • 当A应用调用ContentResolver的query()方法时,实际上相当于调用了该Uri对应的ContentProvider(属于B应用)的query()方法。

通过上述关系,A应用能够访问和操作B应用的底层数据。

9.2.2 开发ContentProvider子类

开发ContentProvider只需要以下两步:

  1. 开发一个ContentProvider子类:该子类需要实现query()insert()update()delete()等方法。
  2. 在AndroidManifest.xml文件中注册该ContentProvider:需要指定android:authorities属性。

在这两步中,ContentProvider子类实现的query()insert()update()delete()方法,并不是供该应用本身调用的,而是供其他应用调用的。正如前面提到的,当其他应用通过ContentResolver调用query()insert()update()delete()方法进行数据访问时,实际上就是调用指定Uri对应的ContentProvider的query()insert()update()delete()方法。

如何实现ContentProvider的query()insert()update()delete()方法,完全由程序员决定。程序员可以按照自己想要暴露数据的方式来实现这4个方法。在极端情况下,开发者只对这些方法提供空实现也是可以的。

例如下面的示例ContentProvider,该ContentProvider虽然实现了query()insert()update()delete()方法,但并未真正对底层数据进行访问,只是输出了一行字符串。下面是该ContentProvider的代码:

public class FirstProvider extends ContentProvider {
    // 第一次创建该ContentProvider时调用该方法
    @Override
    public boolean onCreate() {
        System.out.println("===onCreate方法被调用===");
        return true;
    }

    // 该方法的返回值代表了该ContentProvider所提供数据的MIME类型
    @Override
    public String getType(Uri uri) {
        return null;
    }

    // 实现查询方法,该方法应该返回查询得到的Cursor
    @Override
    public Cursor query(Uri uri, String[] projection, String where, String[] whereArgs, String sortOrder) {
        System.out.println(uri.toString() + "===query方法被调用===");
        System.out.println("where参数为: " + where);
        return null;
    }

    // 实现插入方法,该方法应该返回新插入的记录的Uri
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        System.out.println("values参数为: " + values);
        System.out.println(uri.toString() + "===insert方法被调用===");
        return null;
    }

    // 实现更新方法,该方法应该返回被更新的记录条数
    @Override
    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
        System.out.println(uri.toString() + "===update方法被调用===");
        System.out.println("where参数为: " + where + ", values参数为: " + values);
        return 0;
    }

    // 实现删除方法,该方法应该返回被删除的记录条数
    @Override
    public int delete(Uri uri, String where, String[] whereArgs) {
        System.out.println(uri.toString() + "===delete方法被调用===");
        System.out.println("where参数为: " + where);
        return 0;
    }
}

上面4个粗体字方法实现了query()insert()update()delete()方法,这4个方法用于供其他应用通过ContentResolver调用。在该ContentProvider供其他应用调用之前,还需要先配置该ContentProvider。

9.2.3 配置ContentProvider

Android应用要求所有应用程序组件(Activity、Service、ContentProvider、BroadcastReceiver)都必须显式进行配置。只需要在<application ... />元素中添加<provider ... />子元素即可配置ContentProvider。在配置ContentProvider时通常指定如下属性:

  • name:指定该ContentProvider的实现类的类名。
  • authorities:指定该ContentProvider对应的Uri(相当于为该ContentProvider分配一个域名)。
  • android:exported:指定该ContentProvider是否允许其他应用调用。如果将该属性设为false,那么该ContentProvider将不允许其他应用调用。
  • readPermission:指定读取该ContentProvider所需要的权限。也就是调用ContentProvider的query()方法所需要的权限。
  • writePermission:指定写入该ContentProvider所需要的权限。也就是调用ContentProvider的insert()delete()update()方法所需要的权限。
  • permission:该属性相当于同时配置readPermissionwritePermission两个权限。

如果不配置上面的readPermissionwritePermissionpermission权限,则表明没有权限限制,这意味着该ContentProvider可以被所有App访问。

为了配置上面的ContentProvider,只需要在<application ... />元素中添加如下子元素即可:

<application
    android:icon="@mipmap/ic_launcher">
    <!-- 注册一个ContentProvider -->
    <provider
        android:name=".FirstProvider"
        android:authorities="org.crazyit.providers.firstprovider"
        android:exported="true" />
</application>

上面的配置指定了该ContentProvider被绑定到content://org.crazyit.providers.firstprovider。这意味着当其他应用的ContentResolver向该Uri执行query()insert()update()delete()方法时,实际上是调用该ContentProvider的query()insert()update()delete()方法。

  • ContentResolver调用方法时的参数将会传递给该ContentProvider的query()insert()update()delete()方法。
  • ContentResolver调用方法的返回值,也就是ContentProvider执行query()insert()update()delete()方法的返回值。

提示:

由于该应用并未提供Activity,因此该应用没有任何界面。在Android Studio中运行该应用时,Android Studio可能会显示如图9.2所示的提示窗口,只需按照图中指示选择“不加载任何Activity”即可正常部署该应用。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图9.2 选择不加载任何Activity

9.2.4 使用ContentResolver 调用方法

Context 类提供了 getContentResolver() 方法,这意味着 ActivityService 等组件都可以通过 getContentResolver() 方法获取 ContentResolver 对象。获取了 ContentResolver 对象之后,就可以调用 ContentResolverquery()insert()update()delete() 方法,这实际上是调用指定 Uri 对应的 ContentProviderquery()insert()update()delete() 方法。

在下面的示例中,界面布局文件中包含了4个按钮,分别用于触发调用 ContentProviderquery()insert()update()delete() 方法。由于该示例的界面布局文件很简单,这里不再给出代码。

以下是该示例的 Activity 代码:

public class MainActivity extends Activity {
    private Uri uri = Uri.parse("content://org.crazyit.providers.firstprovider/");

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void query(View source) {
        // 调用ContentResolver的query()方法
        // 实际返回的是该Uri对应的ContentProvider的query()的返回值
        Cursor c = getContentResolver().query(uri, null, "query_where", null, null);
        Toast.makeText(this, "远程ContentProvider返回的Cursor为: " + c, Toast.LENGTH_SHORT).show();
    }

    public void insert(View source) {
        ContentValues values = new ContentValues();
        values.put("name", "fkjava");
        // 调用ContentResolver的insert()方法
        // 实际返回的是该Uri对应的ContentProvider的insert()的返回值
        Uri newUri = getContentResolver().insert(uri, values);
        Toast.makeText(this, "远程ContentProvider新插入记录的Uri为: " + newUri, Toast.LENGTH_SHORT).show();
    }

    public void update(View source) {
        ContentValues values = new ContentValues();
        values.put("name", "fkjava");
        // 调用ContentResolver的update()方法
        // 实际返回的是该Uri对应的ContentProvider的update()的返回值
        int count = getContentResolver().update(uri, values, "update_where", null);
        Toast.makeText(this, "远程ContentProvider更新记录数为: " + count, Toast.LENGTH_SHORT).show();
    }

    public void delete(View source) {
        // 调用ContentResolver的delete()方法
        // 实际返回的是该Uri对应的ContentProvider的delete()的返回值
        int count = getContentResolver().delete(uri, "delete_where", null);
        Toast.makeText(this, "远程ContentProvider删除记录数为: " + count, Toast.LENGTH_SHORT).show();
    }
}

上面的代码通过 ContentResolver 调用 query()insert()update()delete() 方法,实际上就是调用 uri 参数对应的 ContentProviderquery()insert()update()delete() 方法,也就是前面的 FirstProviderquery()insert()update()delete() 方法。

运行上面的程序,并依次单击应用的4个按钮,将可以看到Logcat生成的输出,如图9.3所示。

图9.3 ContentProvider的方法被调用

从图9.3的输出可以看出,当Android应用通过ContentResolver调用query()insert()update()delete()方法时,实际上就是调用FirstProvider(该Uri对应的ContentProvider)的query()insert()update()delete()方法。

当用户单击程序界面上的“插入”按钮时,将可以在程序界面中看到如图9.4所示的输出。

图9.4 通过ContentResolver调用ContentProvider的方法

从图9.4可以看出,调用ContentResolverinsert()方法的返回值为null,这是因为FirstProvider实现insert()方法时返回了null

9.2.5 创建ContentProvider的说明

通过上面的介绍可以看出,ContentProvider不像Activity那样有复杂的生命周期。ContentProvider只有一个onCreate()生命周期方法——当其他应用通过ContentResolver第一次访问该ContentProvider时,onCreate()方法将会被回调,并且onCreate()方法只会被调用一次。ContentProvider提供的query()insert()update()delete()方法则由其他应用通过ContentResolver调用。

在开发ContentProvider时,query()insert()update()delete()方法的第一个参数为Uri,该参数由ContentResolver在调用这些方法时传入。在前面的示例中,由于并未真正对数据进行操作,因此ContentProvider并未对Uri参数进行任何判断。然而,在实际开发中,为了确定ContentProvider实际能够处理的Uri,并确定每个方法中Uri参数所操作的数据,Android系统提供了UriMatcher工具类。

UriMatcher工具类

UriMatcher工具类主要提供了以下两个方法:

  • void addURI(String authority, String path, int code):该方法用于向UriMatcher对象注册Uri。其中authoritypath组合成一个Uri,而code则代表该Uri对应的标识码。
  • int match(Uri uri):根据前面注册的Uri来判断指定Uri对应的标识码。如果找不到匹配的标识码,该方法将会返回-1。

例如,可以通过如下代码来创建UriMatcher对象:

UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
matcher.addURI("org.crazyit.providers.dictprovider", "words", 1);
matcher.addURI("org.crazyit.providers.dictprovider", "word/#", 2);

上面创建的UriMatcher对象注册了两个Uri,其中org.crazyit.providers.dictprovider/words对应的标识码为1;org.crazyit.providers.dictprovider/word/#对应的标识码为2(#为通配符)。

这就意味着如下匹配结果:

matcher.match(Uri.parse("content://org.crazyit.providers.dictprovider/words"));
// 返回标识码1
matcher.match(Uri.parse("content://org.crazyit.providers.dictprovider/word/2"));
// 返回标识码2
matcher.match(Uri.parse("content://org.crazyit.providers.dictprovider/word/10"));
// 返回标识码2

需要为UriMatcher对象注册多少个Uri,取决于系统的业务需求。对于content://org.crazyit.providers.dictprovider/words这个Uri,它的资源部分为words,通常代表访问所有数据项;而对于content://org.crazyit.providers.dictprovider/word/2这个Uri,它的资源部分通常代表访问指定的数据项,其中最后一个数值通常代表该数据的ID。

ContentUris工具类

此外,Android还提供了一个ContentUris工具类,它是一个操作Uri字符串的工具类,提供了如下两个工具方法:

  • withAppendedId(Uri uri, long id):用于为路径加上ID部分。例如:

    Uri uri = Uri.parse("content://org.crazyit.providers.dictprovider/word");
    Uri resultUri = ContentUris.withAppendedId(uri, 2);
    // 生成后的Uri为:"content://org.crazyit.providers.dictprovider/word/2"
    
  • parseId(Uri uri):用于从指定Uri中解析出所包含的ID值。例如:

    Uri uri = Uri.parse("content://org.crazyit.providers.dictprovider/word/2");
    int wordId = ContentUris.parseId(uri); // 获取的结果为2
    

掌握了上述知识后,接下来可以模拟开发一个英文“单词本”应用,并通过ContentProvider暴露它的数据访问接口,这样即可允许其他应用程序通过ContentResolver来操作“单词本”应用的数据。

实例:使用ContentProvider共享单词数据

本实例将为一个“单词本”应用添加ContentProvider,这样其他应用即可通过ContentProvider向该“单词本”添加单词、查询单词。

该“单词本”应用的开发其实是对本地SQLite数据库的CRUD操作。为了方便其他应用访问ContentProvider,一般会将ContentProvider的Uri、数据列等信息以常量的形式公开出来。为此,我们定义了一个Words命名对象(对应于Java的静态工具类),该类中只包含一些public static的常量。

Words 工具类

以下是该工具类的代码:

public final class Words {
    // 定义该ContentProvider的 Authorities
    public static final String AUTHORITY = "org.crazyit.providers.dictprovider";

    // 定义一个静态内部类,定义该ContentProvider所包含的数据列的列名
    public static final class Word implements BaseColumns {
        // 定义Content所允许操作的三个数据列
        public final static String _ID = "_id";
        public final static String WORD = "word";
        public final static String DETAIL = "detail";

        // 定义该ContentProvider提供服务的两个Uri
        public final static Uri DICT_CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/words");
        public final static Uri WORD_CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/word");
    }
}

这个工具类主要是为其他应用程序提供访问该ContentProvider的常用入口。

提示:

虽然我们可以不提供Words工具类,而是在ContentProvider中直接使用字符串来定义Uri,并通过文档告知其他应用程序如何访问该ContentProvider,但这种方式的可维护性较差。因此,在实际项目中(包括Android系统的ContentProvider),通常会通过工具类来定义各种常量。

开发ContentProvider子类

接下来,我们开发一个ContentProvider子类,并重写其中的增、删、改、查等方法。以下是DictProvider类的代码:

public class DictProvider extends ContentProvider {
    private static final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
    private static final int WORDS = 1;
    private static final int WORD = 2;
    private MyDatabaseHelper dbOpenHelper;

    static {
        // 为UriMatcher注册两个Uri
        matcher.addURI(Words.AUTHORITY, "words", WORDS);
        matcher.addURI(Words.AUTHORITY, "word/#", WORD);
    }

    // 第一次调用该DictProvider时,系统先创建DictProvider对象,并回调该方法
    @Override
    public boolean onCreate() {
        dbOpenHelper = new MyDatabaseHelper(this.getContext(), "myDict.db3", 1);
        return true;
    }

    // 返回指定Uri参数对应的数据的MIME类型
    @Override
    public String getType(@NonNull Uri uri) {
        switch (matcher.match(uri)) {
            case WORDS:
                // 如果操作的数据是多项记录
                return "vnd.android.cursor.dir/org.crazyit.dict";
            case WORD:
                // 如果操作的数据是单项记录
                return "vnd.android.cursor.item/org.crazyit.dict";
            default:
                throw new IllegalArgumentException("未知Uri: " + uri);
        }
    }

    // 查询数据的方法
    @Override
    public Cursor query(@NonNull Uri uri, String[] projection, String where, String[] whereArgs, String sortOrder) {
        SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
        switch (matcher.match(uri)) {
            case WORDS:
                // 如果Uri参数代表操作全部数据项
                return db.query("dict", projection, where, whereArgs, null, null, sortOrder);
            case WORD:
                // 如果Uri参数代表操作指定数据项
                long id = ContentUris.parseId(uri);
                String whereClause = Words.Word._ID + "=" + id;
                if (where != null && !"".equals(where)) {
                    whereClause = whereClause + " and " + where;
                }
                return db.query("dict", projection, whereClause, whereArgs, null, null, sortOrder);
            default:
                throw new IllegalArgumentException("未知 Uri: " + uri);
        }
    }

    // 插入数据的方法
    @Override
    public Uri insert(@NonNull Uri uri, ContentValues values) {
        SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
        switch (matcher.match(uri)) {
            case WORDS:
                // 插入数据,返回插入记录的ID
                long rowId = db.insert("dict", Words.Word._ID, values);
                if (rowId > 0) {
                    // 在已有的Uri的后面追加ID
                    Uri wordUri = ContentUris.withAppendedId(uri, rowId);
                    // 通知数据已经改变
                    getContext().getContentResolver().notifyChange(wordUri, null);
                    return wordUri;
                }
                break;
            default:
                throw new IllegalArgumentException("未知Uri: " + uri);
        }
        return null;
    }

    // 修改数据的方法
    @Override
    public int update(@NonNull Uri uri, ContentValues values, String where, String[] whereArgs) {
        SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
        int num;
        switch (matcher.match(uri)) {
            case WORDS:
                // 如果Uri参数代表操作全部数据项
                num = db.update("dict", values, where, whereArgs);
                break;
            case WORD:
                // 如果Uri参数代表操作指定数据项
                long id = ContentUris.parseId(uri);
                String whereClause = Words.Word._ID + "=" + id;
                if (where != null && !"".equals(where)) {
                    whereClause = whereClause + " and " + where;
                }
                num = db.update("dict", values, whereClause, whereArgs);
                break;
            default:
                throw new IllegalArgumentException("未知 Uri: " + uri);
        }
        // 通知数据已经改变
        getContext().getContentResolver().notifyChange(uri, null);
        return num;
    }

    // 删除数据的方法
    @Override
    public int delete(@NonNull Uri uri, String where, String[] whereArgs) {
        SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
        int num;
        switch (matcher.match(uri)) {
            case WORDS:
                // 如果Uri参数代表操作全部数据项
                num = db.delete("dict", where, whereArgs);
                break;
            case WORD:
                // 如果Uri参数代表操作指定数据项
                long id = ContentUris.parseId(uri);
                String whereClause = Words.Word._ID + "=" + id;
                if (where != null && !"".equals(where)) {
                    whereClause = whereClause + " and " + where;
                }
                num = db.delete("dict", whereClause, whereArgs);
                break;
            default:
                throw new IllegalArgumentException("未知Uri: " + uri);
        }
        // 通知数据已经改变
        getContext().getContentResolver().notifyChange(uri, null);
        return num;
    }
}

DictProvider类继承了系统的ContentProvider,实现了数据的增、删、改、查等方法。它通过SQLiteDatabase对底层数据执行这些操作,这样当其他应用通过ContentResolver来调用DictProviderquery()insert()update()delete()方法时,就可以真正访问和操作该ContentProvider所在应用的底层数据。

注册ContentProvider

接下来,需要在AndroidManifest.xml文件中注册该ContentProvider。我们将对该ContentProvider进行权限限制,因此在AndroidManifest.xml文件中增加如下配置片段:

<!-- 注册一个ContentProvider -->
<provider 
    android:name=".DictProvider"
    android:authorities="org.crazyit.providers.dictprovider"
    android:permission="org.crazyit.permission.USE_DICT"
    android:exported="true" />

从上面的配置可以看出,其他应用访问该ContentProvider需要org.crazyit.permission.USE_DICT权限。为了让Android系统知道该权限,还必须在AndroidManifest.xml文件的根元素下(与<application ... />元素同级)增加如下配置:

<!-- 指定该应用暴露了一个权限 -->
<permission 
    android:name="org.crazyit.permission.USE_DICT"
    android:protectionLevel="normal" />

至此,暴露数据的ContentProvider开发完成。为了测试该ContentProvider的开发是否成功,接下来再开发一个应用程序,该应用程序将通过ContentResolver来操作“单词本”中的数据。

测试ContentProvider的应用

该程序提供了添加单词和查询单词的功能,只是该程序自己不保存数据,而是访问前面DictProvider所共享的数据。以下是使用ContentResolver的类的代码:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        EditText keyEt = findViewById(R.id.key);
        EditText detailEt = findViewById(R.id.detail);
        EditText wordEt = findViewById(R.id.word);
        Button insertBn = findViewById

(R.id.insert);
        Button searchBn = findViewById(R.id.search);

        // 为insertBn按钮的单击事件绑定事件监听器
        insertBn.setOnClickListener(view -> {
            // 获取用户输入
            String detail = detailEt.getText().toString();
            String word = wordEt.getText().toString();

            // 插入单词记录
            ContentValues values = new ContentValues();
            values.put(Words.Word.WORD, word);
            values.put(Words.Word.DETAIL, detail);
            getContentResolver().insert(Words.Word.DICT_CONTENT_URI, values);

            // 显示提示信息
            Toast.makeText(MainActivity.this, "添加单词成功!", Toast.LENGTH_SHORT).show();
        });

        // 为searchBn按钮的单击事件绑定事件监听器
        searchBn.setOnClickListener(view -> {
            // 获取用户输入
            String key = keyEt.getText().toString();

            // 执行查询
            Cursor cursor = getContentResolver().query(
                Words.Word.DICT_CONTENT_URI, 
                null, 
                "word LIKE ? OR detail LIKE ?", 
                new String[]{"%" + key + "%", "%" + key + "%"}, 
                null
            );

            // 创建一个Bundle对象
            Bundle data = new Bundle();
            data.putSerializable("data", convertCursorToList(cursor));

            // 创建一个Intent
            Intent intent = new Intent(MainActivity.this, ResultActivity.class);
            intent.putExtras(data);

            // 启动Activity
            startActivity(intent);
        });
    }

    private ArrayList<Map<String, String>> convertCursorToList(Cursor cursor) {
        ArrayList<Map<String, String>> result = new ArrayList<>();
        // 遍历Cursor结果集
        while (cursor.moveToNext()) {
            // 将结果集中的数据存入ArrayList中
            Map<String, String> map = new HashMap<>();
            map.put("word", cursor.getString(1));
            map.put("detail", cursor.getString(2));
            result.add(map);
        }
        return result;
    }
}

上面的代码中,第一段用于向DictProvider所共享的数据中添加记录;第二段则用于查询DictProvider所共享的数据。

运行该程序需要先部署前面介绍的DictProvider,因为该程序所操作的数据实际上来自DictProvider。当用户通过该程序添加单词时,实际上是添加到了DictProvider应用中;当用户通过该程序查询单词时,实际上查询的是DictProvider应用中的单词。

由于前面的ContentProvider声明了使用字典需要org.crazyit.permission.USE_DICT权限,因此该应用必须使用如下代码来声明本应用所需的权限:

<uses-permission android:name="org.crazyit.permission.USE_DICT"/>

9.3 操作系统的ContentProvider

Android系统本身提供了大量的ContentProvider,例如联系人信息、系统的多媒体信息等。开发者可以通过ContentResolver调用系统ContentProvider提供的query()insert()update()delete()方法,从而获取和操作Android内部的数据。

使用ContentResolver操作系统ContentProvider数据的步骤如下:

  1. 获取ContentResolver对象:调用ContextgetContentResolver()方法。
  2. 操作数据:根据需要调用ContentResolverinsert()delete()update()query()方法。

提示:
为了操作系统提供的ContentProvider,开发者需要了解该ContentProvider的Uri,以及该ContentProvider所操作的数据列的列名。可以通过查阅Android官方文档来获取这些信息。

9.3.1 使用ContentProvider管理联系人

Android系统提供了Contacts应用程序来管理联系人,并且为联系人管理提供了ContentProvider,这允许其他应用程序通过ContentResolver来管理联系人数据。

用于管理联系人的ContentProvider的几个Uri如下:

  • ContactsContract.Contacts.CONTENT_URI:管理联系人的Uri。
  • ContactsContract.CommonDataKinds.Phone.CONTENT_URI:管理联系人的电话的Uri。
  • ContactsContract.CommonDataKinds.Email.CONTENT_URI:管理联系人的E-mail的Uri。

了解了联系人管理ContentProvider的Uri之后,就可以在应用程序中通过ContentResolver操作系统的联系人数据了。

下面是一个示例程序,其中包含两个按钮,一个用于查询系统的联系人数据,另一个用于添加联系人数据。

查询联系人数据

查询联系人数据的按钮所绑定的事件监听器代码如下:

searchBn.setOnClickListener(view -> 
    // 请求读取联系人信息的权限
    requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, 0x123)
);

上述事件监听器向用户请求获取读取联系人信息的权限,因为Android要求应用访问联系人信息时必须在运行时请求获取权限。

接下来程序在onRequestPermissionsResult方法中根据用户授权进行处理:当用户授权读取联系人信息时,程序查询联系人信息。下面是onRequestPermissionsResult方法中查询联系人信息的代码:

// 定义两个List来封装系统的联系人信息、指定联系人的电话号码、E-mail等详情
List<String> names = new ArrayList<>();
List<List<String>> details = new ArrayList<>();

// 使用ContentResolver查询联系人数据
Cursor cursor = getContentResolver().query(ContactsContract.Contacts.CONTENT_URI, null, null, null, null);

// 遍历查询结果,获取系统中所有联系人
while (cursor.moveToNext()) {
    // 获取联系人ID
    String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));
    // 获取联系人的名字
    String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
    names.add(name);

    // 使用ContentResolver 查询联系人的电话号码
    Cursor phones = getContentResolver().query(
        ContactsContract.CommonDataKinds.Phone.CONTENT_URI, 
        null, 
        ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=" + contactId,
        null, 
        null
    );

    // 遍历查询结果,获取该联系人的多个电话号码
    List<String> detail = new ArrayList<>();
    while (phones.moveToNext()) {
        // 获取查询结果中电话号码列中的数据
        String phoneNumber = phones.getString(phones.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
        detail.add("电话号码:" + phoneNumber);
    }
    phones.close();

    // 使用ContentResolver查询联系人的E-mail地址
    Cursor emails = getContentResolver().query(
        ContactsContract.CommonDataKinds.Email.CONTENT_URI, 
        null, 
        ContactsContract.CommonDataKinds.Email.CONTACT_ID + "=" + contactId, 
        null, 
        null
    );

    // 遍历查询结果,获取该联系人的多个E-mail地址
    while (emails.moveToNext()) {
        // 获取查询结果中E-mail地址列中的数据
        String emailAddress = emails.getString(emails.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA));
        detail.add("邮件地址:" + emailAddress);
    }
    emails.close();

    details.add(detail);
}
cursor.close();

// 加载result.xml界面布局代表的视图
View resultDialog = getLayoutInflater().inflate(R.layout.result, null);
// 获取resultDialog中ID为list的ExpandableListView
ExpandableListView list = resultDialog.findViewById(R.id.list);

// 创建一个ExpandableListAdapter对象
ExpandableListAdapter adapter = new BaseExpandableListAdapter() {
    @Override
    public int getGroupCount() {
        return names.size();
    }

    @Override
    public int getChildrenCount(int groupPosition) {
        return details.get(groupPosition).size();
    }

    @Override
    public Object getGroup(int groupPosition) {
        return names.get(groupPosition);
    }

    @Override
    public Object getChild(int groupPosition, int childPosition) {
        return details.get(groupPosition).get(childPosition);
    }

    @Override
    public long getGroupId(int groupPosition) {
        return groupPosition;
    }

    @Override
    public long getChildId(int groupPosition, int childPosition) {
        return childPosition;
    }

    @Override
    public boolean hasStableIds() {
        return true;
    }

    // 该方法决定每个组选项的外观
    @Override
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
        TextView textView;
        if (convertView == null) {
            textView = createTextView();
        } else {
            textView = (TextView) convertView;
        }
        textView.setTextSize(18f);
        textView.setPadding(90, 10, 0, 10);
        textView.setText(getGroup(groupPosition).toString());
        return textView;
    }

    // 该方法决定每个子选项的外观
    @Override
    public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
        TextView textView;
        if (convertView == null) {
            textView = createTextView();
        } else {
            textView = (TextView) convertView;
        }
        textView.setText(getChild(groupPosition, childPosition).toString());
        return textView;
    }

    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return true;
    }

    private TextView createTextView() {
        AbsListView.LayoutParams lp = new AbsListView.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
        TextView textView = new TextView(MainActivity.this);
        textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
        textView.setLayoutParams(lp);
        textView.setPadding(40, 5, 0, 5);
        textView.setTextSize(15f);
        return textView;
    }
};

// 为ExpandableListView设置Adapter对象
list.setAdapter(adapter);

// 使用对话框来显示查询结果
new AlertDialog.Builder(MainActivity.this)
    .setView(resultDialog)
    .setPositiveButton("确定", null).show();

上述程序使用ContentResolverContactsContract.Contacts.CONTENT_URI中查询数据,获取系统中所有联系人信息;然后从ContactsContract.CommonDataKinds.Phone.CONTENT_URI中查询电话号码,从ContactsContract.CommonDataKinds.Email.CONTENT_URI中查询E-mail信息。最后,使用ExpandableListView显示所有联系人信息。

需要注意的是,上述应用程序需要读取、添加联系人信息,因此需要在AndroidManifest.xml文件中为该应用程序授权。即在文件的根元素中添加以下元素:

<!-- 授予读联系人ContentProvider的权限 -->
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<!-- 授予写联系人ContentProvider的权限 -->
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>

运行该程序并单击“查询”按钮,程序将使用ExpandableListView显示所有联系人信息。

添加联系人数据

应用程序界面提供了三个文本框,用户可以在这些文本框中输入联系人名字、电话号码、E-mail地址,然后单击“添加”按钮。该按钮绑定的事件监听器代码如下:

// 为addBn按钮的单击事件绑定监听器
addBn.setOnClickListener(view -> 
    // 请求写入联系人信息的权限
    requestPermissions(new String[]{Manifest.permission.WRITE_CONTACTS}, 0x456)
);

上述事件监听器请求用户获取写入联系人信息的权限。接下来程序在onRequestPermissionsResult方法中根据用户授权进行处理:当用户授权写入联系人信息时,程序就添加联系人。以下是在onRequestPermissionsResult方法中添加联系人的代码:

// 获取程序界面中的三个文本框的内容
String name = ((EditText) findViewById(R.id.name)).getText().toString().trim();
String phone = ((EditText) findViewById(R.id.phone)).getText().toString().trim();
String email = ((EditText) findViewById(R.id.email)).getText().toString().trim();
if ("".equals(name)) return;

// 创建一个空的

ContentValues
ContentValues values = new ContentValues();
// 向RawContacts.CONTENT_URI执行一个空值插入,目的是获取系统返回的rawContactId
Uri rawContactUri = getContentResolver().insert(ContactsContract.RawContacts.CONTENT_URI, values);
values.clear();
long rawContactId = ContentUris.parseId(rawContactUri);

// 设置内容类型
values.put(ContactsContract.RawContacts.Data.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.RawContacts.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
// 设置联系人名字
values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, name);
// 向联系人URI添加联系人名字
getContentResolver().insert(android.provider.ContactsContract.Data.CONTENT_URI, values);
values.clear();

// 设置电话号码内容类型
values.put(ContactsContract.RawContacts.Data.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.RawContacts.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
// 设置联系人的电话号码
values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, phone);
// 设置电话类型
values.put(ContactsContract.CommonDataKinds.Phone.TYPE,
        ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE);
// 向联系人电话号码URI添加电话号码
getContentResolver().insert(android.provider.ContactsContract.Data.CONTENT_URI, values);
values.clear();

// 设置E-mail内容类型
values.put(ContactsContract.RawContacts.Data.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.RawContacts.Data.MIMETYPE,
        ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
// 设置联系人的E-mail地址
values.put(ContactsContract.CommonDataKinds.Email.DATA, email);
// 设置该电子邮件的类型
values.put(ContactsContract.CommonDataKinds.Email.TYPE,
        ContactsContract.CommonDataKinds.Email.TYPE_WORK);
// 向联系人E-mail URI添加E-mail数据
getContentResolver().insert(android.provider.ContactsContract.Data.CONTENT_URI, values);

// 显示提示信息
Toast.makeText(MainActivity.this, "联系人数据添加成功", Toast.LENGTH_SHORT).show();

上述代码通过ContentResolver添加联系人信息:首先向RawContacts.CONTENT_URI添加一条空记录用于获取新添加联系人的URI,然后为指定联系人添加名字、电话号码和E-mail地址。

提示:

从底层设计来看,Android的联系人信息是一张主表,电话信息和E-mail信息是两张从表(都参照联系人表)。因此,开发者可以分别为一个联系人添加多个电话号码和多个E-mail信息。

通过这些代码,当用户在程序的三个文本框中分别输入联系人姓名、电话号码、E-mail地址,并单击“添加”按钮后,联系人信息将被添加到系统中。在Android系统的Contacts应用中,可以查看新添加的联系人信息。

9.3.2 使用ContentProvider管理多媒体内容

Android提供了Camera程序支持拍照和拍摄视频,用户拍摄的照片和视频存放在固定的位置。为了让其他应用程序可以访问这些照片和视频,Android为多媒体内容提供了ContentProvider。

以下是Android为多媒体提供的ContentProvider的Uri:

  • MediaStore.Audio.Media.EXTERNAL_CONTENT_URI:存储在外部存储器(SD卡)上的音频文件内容的ContentProvider的Uri。
  • MediaStore.Audio.Media.INTERNAL_CONTENT_URI:存储在手机内部存储器上的音频文件内容的ContentProvider的Uri。
  • MediaStore.Images.Media.EXTERNAL_CONTENT_URI:存储在外部存储器(SD卡)上的图片文件内容的ContentProvider的Uri。
  • MediaStore.Images.Media.INTERNAL_CONTENT_URI:存储在手机内部存储器上的图片文件内容的ContentProvider的Uri。
  • MediaStore.Video.Media.EXTERNAL_CONTENT_URI:存储在外部存储器(SD卡)上的视频文件内容的ContentProvider的Uri。
  • MediaStore.Video.Media.INTERNAL_CONTENT_URI:存储在手机内部存储器上的视频文件内容的ContentProvider的Uri。

示例程序:管理多媒体内容

下面是一个示例程序的界面,包含两个按钮:一个“查看”按钮,用于查看多媒体数据中的所有图片;一个“添加”按钮,用于向多媒体数据中添加图片。

查看图片信息

“查看”按钮所绑定的事件监听器的代码如下:

// 为viewBn按钮的单击事件绑定监听器
viewBn.setOnClickListener(view -> 
    // 请求读取外部存储器的权限
    requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 0x123)
);

该事件监听器请求用户获取读取外部存储器的权限。接下来程序在onRequestPermissionsResult方法中根据用户授权进行处理:当用户授权读取外部存储器时,程序读取外部存储器中的图片信息。以下是onRequestPermissionsResult方法中读取图片信息的代码:

// 清空 names, descs、fileNames集合里原有的数据
names.clear();
descs.clear();
fileNames.clear();

// 通过ContentResolver查询所有图片信息
Cursor cursor = getContentResolver().query(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);

while (cursor.moveToNext()) {
    // 获取图片的显示名
    String name = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME));
    // 获取图片的详细描述
    String desc = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DESCRIPTION));
    // 获取图片的保存位置的数据
    String filePath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
    
    // 将图片名添加到names集合中
    names.add(name == null ? "null" : name);
    // 将图片描述添加到descs集合中
    descs.add(desc == null ? "null" : desc);
    // 将图片保存路径添加到fileNames集合中
    fileNames.add(filePath);
}
cursor.close();

RecyclerView.Adapter adapter = new RecyclerView.Adapter<LineViewHolder>() {
    @NonNull @Override
    public LineViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
        View itemView = getLayoutInflater().inflate(R.layout.line,
            new LinearLayout(MainActivity.this), false);
        return new LineViewHolder(itemView);
    }

    @Override
    public void onBindViewHolder(@NonNull LineViewHolder lineViewHolder, int i) {
        lineViewHolder.nameView.setText(names.get(i));
        lineViewHolder.descView.setText(descs.get(i));
    }

    @Override
    public int getItemCount() {
        return names.size();
    }
};

// 为RecyclerView设置Adapter对象
show.setAdapter(adapter);

上面的代码使用ContentResolverMediaStore.Images.Media.EXTERNAL_CONTENT_URI查询数据,这将查询出所有位于外部存储器上的图片信息。查询出图片信息之后,程序使用RecyclerView来显示这些图片信息。

该应用需要读取外部存储设备中的多媒体信息,因此必须为该应用增加读取外部存储设备的权限。此外,该应用还需要向外部存储设备写入图片,因此也需要增加写入外部存储设备的权限。需要在AndroidManifest.xml文件中增加以下配置:

<!-- 授予读取外部存储设备的访问权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 授予写入外部存储设备的访问权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

运行该程序并单击“查看”按钮之后,系统将会显示一个包含所有图片信息的列表。

查看指定图片

RecyclerView中的每个列表项显示图片的名称和描述,并且可以单击查看具体的图片。LineViewHolder内部类为每个列表项的单击事件绑定了事件监听器,代码如下:

class LineViewHolder extends RecyclerView.ViewHolder {
    TextView nameView, descView;

    public LineViewHolder(@NonNull View itemView) {
        super(itemView);
        nameView = itemView.findViewById(R.id.name);
        descView = itemView.findViewById(R.id.desc);
        
        // 设置列表项的单击事件
        itemView.setOnClickListener(view -> {
            // 加载view.xml界面布局代表的视图
            View viewDialog = getLayoutInflater().inflate(R.layout.view, null);
            // 获取viewDialog中ID为image的组件
            ImageView image = viewDialog.findViewById(R.id.image);
            // 设置image显示指定图片
            image.setImageBitmap(BitmapFactory.decodeFile(fileNames.get(getAdapterPosition())));
            // 使用对话框显示用户单击的图片
            new AlertDialog.Builder(MainActivity.this)
                .setView(viewDialog)
                .setPositiveButton("确定", null).show();
        });
    }
}

当用户单击指定列表项时,程序会使用一个包含ImageView的对话框来显示该列表项所对应的图片。

添加图片

“添加”按钮用于将本程序中的指定图片添加到多媒体数据中。实际上,可以将任意图片添加到多媒体数据中。以下是为“添加”按钮绑定事件监听器的代码:

// 为addBn按钮的单击事件绑定监听器
addBn.setOnClickListener(view -> 
    // 请求写入外部存储器的权限
    requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0x456)
);

该事件监听器请求用户获取写入外部存储器的权限。接下来程序在onRequestPermissionsResult方法中根据用户授权进行处理:当用户授权写入外部存储器时,程序向外部存储器中添加图片。以下是onRequestPermissionsResult方法中添加图片信息的代码:

// 创建ContentValues对象,准备插入数据
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "jinta");
values.put(MediaStore.Images.Media.DESCRIPTION, "金塔");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
// 插入数据,返回所插入数据对应的Uri
Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

// 加载应用程序下的jinta图片
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.jinta);

try {
    // 获取刚插入的数据的Uri对应的输出流
    OutputStream os = getContentResolver().openOutputStream(uri); // ①
    // 将bitmap图片保存到Uri对应的数据节点中
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
} catch (IOException e) {
    e.printStackTrace();
}

Toast.makeText(MainActivity.this, "图片添加成功", Toast.LENGTH_SHORT).show();

上面的代码使用ContentResolverMediaStore.Images.Media.EXTERNAL_CONTENT_URI插入了一条记录,但此时还未把真正的图片数据插入进去。程序中标记为的代码打开了刚插入图片的Uri对应的OutputStream,然后调用Bitmapcompress方法将实际的图片内容保存到OutputStream中,这样就将实际图片内容存入系统中。

9.4 监听ContentProvider的数据改变

在前面介绍中,我们了解到ContentProvider可以将数据共享出来,而ContentResolver可以根据需要主动查询ContentProvider所共享的数据。不过,有些时候,应用程序需要实时监听ContentProvider所共享数据的改变,并根据数据的改变进行响应,这就需要使用ContentObserver。

9.4.1 ContentObserver简介

当开发ContentProvider时,不管实现insert()delete()update()方法中的哪一个,只要这些方法导致ContentProvider的数据发生改变,程序就会调用以下代码:

context.getContentResolver().notifyChange(uri, null);

这行代码用于通知所有注册在该Uri上的监听者,该ContentProvider所共享的数据发生了改变。

要在应用程序中监听ContentProvider数据的改变,必须使用Android提供的ContentObserver基类。监听ContentProvider数据改变的监听器需要继承ContentObserver类,并重写基类定义的onChange(boolean selfChange)方法。当监听的ContentProvider数据发生改变时,该onChange()方法将会被触发。

要监听指定ContentProvider的数据变化,需要通过ContentResolver向指定Uri注册ContentObserver监听器。ContentResolver提供了以下方法来注册监听器:

registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer)
  • uri:该监听器所监听的ContentProvider的Uri。
  • notifyForDescendants:如果该参数设置为true,假如注册监听的Uri为content://abc,那么Uri为content://abc/xyzcontent://abc/xyz/foo的数据改变时也会触发该监听器;如果设置为false,那么只有content://abc的数据发生改变时才会触发该监听器。
  • observer:监听器实例。

例如,以下代码片段用于为指定Uri注册监听器:

contentResolver.registerContentObserver(Uri.parse("content://sms"), true, new SmsObserver(new Handler()));

这里的SmsObserver就是ContentObserver的子类。

9.4.2 实例:监听用户发出的短信

在这个实例中,通过监听Uri为Telephony.Sms.CONTENT_URI的数据改变即可监听到用户短信的数据改变,并在监听器的onChange()方法里查询Uri为Telephony.Sms.Sent.CONTENT_URI的数据,从而获取用户正在发送的短信(这些短信保存在发件箱中)。

以下是程序的代码:

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 请求获取读取短信的权限
        requestPermissions(new String[]{Manifest.permission.READ_SMS}, 0x123);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions,
                                           @NonNull int[] grantResults) {
        // 如果用户授权访问短信内容
        if (grantResults[0] == PackageManager.PERMISSION_GRANTED && requestCode == 0x123) {
            // 为Telephony.Sms.CONTENT_URI的数据改变注册监听器
            getContentResolver().registerContentObserver(
                Telephony.Sms.CONTENT_URI, true, new SmsObserver(new Handler()));
        } else {
            Toast.makeText(this, "您必须授权访问短信内容才能测试该应用", Toast.LENGTH_SHORT).show();
        }
    }

    // 自定义ContentObserver监听器类
    private class SmsObserver extends ContentObserver {
        private String prevMsg = "";

        SmsObserver(Handler handler) {
            super(handler);
        }

        @Override
        public void onChange(boolean selfChange) {
            Cursor cursor = getContentResolver().query(Telephony.Sms.Sent.CONTENT_URI, null, null, null, null);
            // 遍历查询结果集,获取用户正在发送的短信
            while (cursor.moveToNext()) {
                // 只显示最近5秒内发出的短信
                if (Math.abs(System.currentTimeMillis() - cursor.getLong(cursor.getColumnIndex("date"))) < 5000) {
                    StringBuilder sb = new StringBuilder();
                    // 获取短信的发送地址
                    sb.append("address-").append(cursor.getString(cursor.getColumnIndex("address")));
                    // 获取短信的标题
                    sb.append(";subject-").append(cursor.getString(cursor.getColumnIndex("subject")));
                    // 获取短信的内容
                    sb.append(";body-").append(cursor.getString(cursor.getColumnIndex("body")));
                    // 获取短信的发送时间
                    sb.append(";time-").append(cursor.getLong(cursor.getColumnIndex("date")));
                    if (!prevMsg.equals(sb.toString())) {
                        System.out.println("发送短信:" + sb.toString());
                        prevMsg = sb.toString();
                    }
                }
            }
            cursor.close();
        }
    }
}

上述代码中的重要部分:

  1. 监听数据改变:第一段粗体字代码用于监听Uri为Telephony.Sms.CONTENT_URI的数据改变,从而监听到用户短信数据的改变。
  2. 查询发件箱数据:第二段粗体字代码用于查询Telephony.Sms.Sent.CONTENT_URI的全部数据,也就是查询发件箱内的全部短信。程序只取出最近5秒内发出的短信数据,这样避免将之前发送的短信提取出来。

运行该程序时,它会向用户请求读取短信的权限。如果用户授予该应用读取短信的权限,该应用即可监听到用户发出的短信。在不关闭该程序的情况下,打开Android系统内置的“Messaging”程序发送短信(可以直接向本机号码发送)。当用户发送短信时,可以在Logcat面板中看到输出,类似于:

发送短信:address-12345;subject-;body-hello;time-1694865314000

由于本程序需要读取系统短信内容,因此还需要在AndroidManifest.xml文件中增加读取短信的权限配置:

<!-- 授予本应用读取短信的权限 -->
<uses-permission android:name="android.permission.READ_SMS"/>

注意: Genymotion模拟器不支持模拟发送短信的功能,因此本实例需要使用Android自带的AVD模拟器进行测试。

这个监听用户发送短信的程序用Activity实现并不合适,因为用户必须先主动打开该Activity,并且在保持该Activity不关闭的情况下,用户所发送的短信才会被监听到。这显然不符合实际需求场景。在实际情况下,更希望该程序以后台进程的方式“不知不觉”地运行,这就需要利用Android的Service组件。接下来的章节将介绍Android的Service组件。

9.5 本章小结

本章主要介绍了Android系统中ContentProvider组件的功能与用法。ContentProvider本质上就像一个“小网站”,它可以按照“固定规范”将应用程序的数据暴露出来,其他应用程序可以通过ContentProvider提供的接口来操作这些数据。因此,ContentProvider是Android系统中不同应用程序之间进行数据交换的标准接口。

学习本章内容时,需重点掌握以下三个API的用法:

  1. ContentResolver:用于操作ContentProvider提供的数据,负责与ContentProvider进行交互,包括查询、插入、更新和删除数据。

  2. ContentProvider:作为所有ContentProvider组件的基类,它定义了数据的CRUD操作(增删改查)方法。通过实现这些方法,开发者可以指定数据的暴露和操作方式。

  3. ContentObserver:用于监听ContentProvider的数据改变。当ContentProvider的数据发生变化时,ContentObserver可以接收通知并作出相应的响应,适合需要实时监控数据变化的场景。

通过掌握这三个API,开发者可以在Android应用程序中实现不同应用程序之间的数据共享和交互。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部