19.内容提供者ContentProvider

之前我们学习了Android数据持久化的技术,包括文件存储(内部存储)、SharedPreferences存储以及数据库存储。

这些持久化技术所保存的数据基本都是在当前应用程序中访问。

Android官方已经不再推荐使用这种方式来实现跨程序数据共享的功能,而是应该使用更加安全可靠的内容提供者技术。

那么,哪些数据可以共享给其他程序呢?这个要视情况而定的,比如说账号和密码这样的隐私数据显然是不能共享给其他程序的,但是有一些可以让其他程序进行二次开发的基础性数据,我们还是可以选择将其共享的。例如,系统的电话簿程序,它的数据库中保存了很多的联系人信息,如果这些数据都不允许第三方的程序进行访问的话,恐怕很多应用的功能都要大打折扣了。除了电话簿之外,还有短信、媒体库等程序都实现了跨程序数据共享的功能,而使用的技术当然就是内容提供者了,下面我们就来学习一下内容提供者。

1、内容提供者简介

内容提供者(ContentProvider)是Android系统组件之一,主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访数据的安全性。

目前,使用内容提供者是Android实现跨程序共享数据的标准方式。

ContentProvider为不同的程序之间数据共享,提供统一的接口。而且ContentProvider是以类似数据库中表的方式将数据暴露,也就是说ContentProvider就像一个“数据库”。

那么外界获取其提供的数据,也就应该与从数据库中获取数据的操作基本一样,只不过是采用URI来表示外界需要访问的“数据库”。

不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,内容提供者可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。为了更好的理解,我们通过下面的图例来讲解内容提供者(ContentProvider)的工作原理。

 

从上图我们可以看出,A程序需要使用内容提供者(ContentProvider)暴露数据,才能被其他程序操作。B程序必须通过内容解析者(ContentResolver)操作A程序暴露出来的数据,而A程序会将操作结果返回给内容解析者(ContentResolver),然后内容解析者再将操作结果返回给B程序。

这里的访问过程,类似于我们使用Windows系统的IE浏览器去访问一个网站的数据库,用户不能直接读取数据库里的数据。

需要借助于系统的IE浏览器(内容解析者,直接从系统取得)去访问某个网址(http://www.sdbi.edu.cn),而服务器的数据需要通过架设的网站对外共享发布(内容提供者,需要自己定义),这个网站可以有多个子页面提供不同的数据(http://www.sdbi.edu.cn/xygk/xyjj.htm或者http://www.sdbi.edu.cn/xsgz/jgjj.htm,这个就相当于内容提供者的路径Uri)。

内容提供者的用法一般有两种:

一种是使用ContentResolver(内容解析者)访问现有的内容提供者暴露的相应程序中的数据;

另一种是创建自己的内容提供者为我们程序的数据提供外部访问接口。

首先从使用现有的内容提供者开始。

2、访问其他程序中的数据

当一个应用程序通过内容提供者对其数据提供了外部访问接口,任何其他的应用程序就都可以对这部分数据进行访问。

Android系统中自带的电话簿、短信、媒体库等程序都提供了类似的访问接口,这就使得第三方应用程序可以充分地利用这部分数据来实现更好的功能。

那么,如何使用现有的内容提供者?

(1)ContentResolver(内容解析者)的基本用法

对于每一个应用程序来说,如果想要访问内容提供者中共享的数据,就一定要借助ContentResolver(内容解析者)类,可以通过Context中的getContentResolver()方法获取到该类的实例。

ContentResolver中提供了一系列的方法用于对数据进行增删改查操作,其中insert()方法用于添加数据,update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据。

是不是和SQLiteDatabase增删改查操作一样,只不过它们在方法参数上稍微有一些区别。

不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI(统一资源标识符)

内容URI给内容提供者中的数据建立了唯一标识符,它主要由三部分组成,协议、权限(authority)和路径(path)。

① 协议:最前面是协议声明“content://”。

② 权限:用于对不同的应用程序做区分的(类似于网站的地址),一般为了避免冲突,都会采用程序包名的方式来进行命名。

  比如某个程序的包名是com.sdbi.app,那么该程序对应的权限就可以命名为com.sdbi.app.provider。

③ 路径:用于对同一应用程序中不同的表做区分的(类似于同一个站点下面的不同栏目),通常都会添加到权限的后面。

  比如某个程序的数据库里存在两张表,table1和table2,这时就可以将路径分别命名为/table1和/table2。

因此,内容URI最标准的格式(3部分)写法如下:

content://com.sdbi.app.provider/table1
content://com.sdbi.app.provider/table2

这个内容URI就类似于我们通过IE浏览器访问某个网站的网址:http://www.sdbi.edu.cn/xsgz/jgjj.htm。

通过这个网址,我们可以清晰的知道,要访问哪个站的哪个页面,同样,内容URI可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据。

所以,ContentResolver中的增删改查方法才都接收Uri对象作为参数,因为使用表名的话系统将无法得知我们期望访问的是哪个应用程序里的表(因为需要跨程序共享数据)。

在得到了内容URI字符串(String类型)之后,我们还需要将它解析成Uri对象才可以作为参数传入。

解析的方法也相当简单,代码如下所示:

Uri uri = Uri.parse("content://com.sdbi.app.provider/table1");

Uri.parse()方法作用是将内容URI字符串解析成Uri对象,这个方法之前学习隐式意图时用到过。

① 查询数据:query()方法

Cursor cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);

这些参数和SQLiteDatabase中query()方法里的参数很像,但总体来说要简单一些,毕竟这是在访问其他程序中的数据,没必要构建过于复杂的查询语句。

下表对使用到的这部分参数进行了详细的解释。

序号

query()方法参数

对应SQL部分

描述

1

uri

from table_name

指定查询某个应用程序下的某一张表

2

projection

select column1, column2

指定查询的列名

3

selection

where column = value

指定where的约束条件

4

selectionArgs

-

为where中的占位符提供具体的值

5

orderBy

order by column1, column2

指定查询结果的排序方式

查询完成后返回的仍然是一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取出来了。

读取的方法仍然是通过移动游标的位置来遍历Cursor的所有行,然后再取出每一行中相应列的数据,代码如下所示:

if (cursor != null && cursor.moveToFirst()) {
    do {
        // 遍历Cursor对象,取出数据并打印
        int index1 = cursor.getColumnIndex("column1");
        String column1 = cursor.getString(index1);
        int index2 = cursor.getColumnIndex("column2");
        int column2 = cursor.getInt(index2);
    } while (cursor.moveToNext());
}
cursor.close(); // 关闭游标

② 添加数据:insert()方法

ContentValues values = new ContentValues();
values.put("column1", "text");
values.put("column2", 1);
getContentResolver().insert(uri, values);

可以看到,仍然是将待添加的数据组装到ContentValues中,然后调用ContentResolver的insert()方法,将Uri和ContentValues作为参数传入即可。

③ 更新数据:update()方法

把column1的值清空:

ContentValues values = new ContentValues();
values.put("column1", "");
getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[] {"text", "1"});

注意上述代码使用了selection和selectionArgs参数来对想要更新的数据进行约束,以防止所有的行都会受影响。

④ 删除数据:delete()方法

getContentResolver().delete(uri, "column2 = ?", new String[] { "1" });

学到这里,你会发现ContentResolver中的增删改查方法和上一章中学习SQLiteDatabase的增删改查很类似,所需特别注意的就只有uri这个参数而已。

那么,我们就利用目前所学的知识,看一看如何读取系统电话簿中的联系人信息。

(2)读取系统联系人

由于我们之前一直使用的都是模拟器,电话簿里面并没有联系人存在,所以现在需要自己手动添加几个,以便稍后进行读取。

打开电话簿程序,目前电话簿里是没有任何联系人的,我们可以通先创建两个联系人,分别填入他们的姓名和手机号。

这样准备工作就做好了,现在新建一个ContactsTest项目,让我们开始动手吧。

首先还是来编写一下布局文件,这里我们希望读取出来的联系人信息能够在ListView中显示,因此,修改activity_main.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<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/lvContacts"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></ListView>
</LinearLayout>

简单起见,LinearLayout里就只放置了一个ListView。

接着修改MainActivity中的代码,如下所示:

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentValues;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    ListView lvContacts;
    ArrayAdapter<String> adapter;
    List<String> contactsList = new ArrayList<String>();

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

        lvContacts = (ListView) findViewById(R.id.lvContacts);
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contactsList);
        lvContacts.setAdapter(adapter);
        readContacts();
    }

    private void readContacts() {
        Cursor cursor = null;
        try {
            // 查询联系人数据
            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
            if (cursor != null && cursor.moveToFirst()) {
                do {
                    // 获取联系人姓名
                    int displayNameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
                    String displayName = cursor.getString(displayNameIndex);
                    // 获取联系人手机号
                    int numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
                    String number = cursor.getString(numberIndex);
                    contactsList.add(displayName + "\n" + number);
                } while (cursor.moveToNext());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }
}

在onCreate()方法中,首先获取了ListView控件的实例,并给它设置好了适配器,然后就去调用readContacts()方法。

下面重点看下readContacts()方法,可以看到,这里使用了ContentResolver的query()方法来查询系统的联系人数据。

不过传入的Uri参数怎么有些奇怪啊,为什么没有调用Uri.parse()方法去解析一个内容URI字符串呢?

这是因为ContactsContract.CommonDataKinds.Phone已经帮我们做好了封装,提供了一个CONTENT_URI常量("content://com.android.contacts/data/phones",数据库位置:/data/data/com.android.providers.contacts/databases/contact2.db),而这个常量就是使用Uri.parse()方法解析出来的结果。

接着我们对Cursor对象进行遍历,将联系人姓名和手机号这些数据逐个取出,联系人姓名这一列对应的常量是ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME("display_name",联系人姓名),联系人手机号这一列对应的常量是ContactsContract.CommonDataKinds.Phone.NUMBER("data1",手机号)。

两个数据都取出之后,将它们进行拼接,并且中间加上换行符,然后将拼接后的数据添加到ListView里。

最后千万不要忘记将Cursor对象关闭掉。

最后注意:读取系统联系人也是需要声明权限的,因此修改AndroidManifest.xml中的代码,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.READ_CONTACTS" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.ContactsTest"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>
    </application>

</manifest>

在清单文件中加入了android.permission.READ_CONTACTS权限后,我们还需要在使用权限的地方检查和动态请求权限(Android 6.0 API 23之后新增)。

这样我们的程序才可以访问到系统的联系人数据了。

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.ContentValues;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.ArrayAdapter;
import android.widget.ListView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    ListView lvContacts;
    ArrayAdapter<String> adapter;
    List<String> contactsList = new ArrayList<String>();

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

        lvContacts = (ListView) findViewById(R.id.lvContacts);
        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contactsList);
        lvContacts.setAdapter(adapter);
        readContacts();
    }

    private void readContacts() {
        Cursor cursor = null;
        try {
            // 检查和动态请求权限
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, 1);
            }
            // 查询联系人数据
            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
            if (cursor != null && cursor.moveToFirst()) {
                do {
                    // 获取联系人姓名
                    int displayNameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
                    String displayName = cursor.getString(displayNameIndex);
                    // 获取联系人手机号
                    int numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
                    String number = cursor.getString(numberIndex);
                    contactsList.add(displayName + "\n" + number);
                } while (cursor.moveToNext());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }
}

来运行一下程序吧,效果如图所示。

      

刚刚添加的联系人的数据都成功读取出来了!说明跨程序访问数据的功能确实是实现了。

下面我们来看一下,联系人数据库中的数据关系。

与联系人相关的主要的表总共有四个:contacts,data,mimetypes,raw_contacts

(一)contacts表

保存了所有的联系人,每个联系人占一行,该表保存了联系人的name_raw_contact_id、联系次数、最后一次联系的时间、是否含有号码、是否被添加到收藏夹等信息。

contacts是由raw_contacts经过整合而来的,一个contacts可以由一个或多个raw_contacts组合而成。

外键关系:contacts表中字段name_raw_contact_id对应着表raw_contacts表中的字段_id。

 (二)data表

保存了所有创建过的手机联系人的所有信息,每个字段占一行。

该表保存了两个ID:mimetype_id和raw_contact_id,从而将data表、raw_contacts表和mimetype表联系起来。

联系人的所有信息保存在列data1至data15中,各列中保存的内容根据raw_contact_id的不同而不同。

外键关系:data表中字段raw_contact_id对应着raw_contacts表中的字段_id,data表中字段mimetype_id对应着mimetype表中的字段_id。

通过这些外键关系,可以找到每一个联系人的多种类别(邮件、电话,姓名等等)对应的数据,data1到data15这些字段保存着联系人的信息、联系人名称、联系人电话号码、电子邮件、备注等等。

例如,如果保存电话号码(mimetype_id=5),data表中的data1字段主要存储具体的数据(电话号码/邮件地址/姓名等等),data2字段保存号码类型(手机号码/家庭号码/工作号码等);如果保存组织机构(mimetype_id=4),data表中的data1字段主要存储公司,data4字段保存职位。

具体字段的含义,大家可以今后在开发过程中逐一的实验了解。

 (三)raw_contacts表

保存了所有创建过的手机联系人,每个联系人占一行,表里有一列(deleted)标识该联系人是否被删除。

该表保存了contact_id,从而将raw_contacts表和contacts表联系起来。

该表保存了联系人的contact_id、联系次数、最后一次联系的时间、是否被添加到收藏夹、显示的名字、用于排序的汉语拼音等信息。

其中字段version(版本),每个联系人的数据修改一次,version就会变化一次(加1),所以可以通过判断version是否改变来监听联系人数据的变化;

字段deleted(删除),默认为0,如果是1表示这行数据已经删除。

 

这个表中的_id字段是关键,通过它可以在data表中查询联系人的电话、姓名、邮箱等信息。

 (四)mimetypes表

提供了16种联系人存储的数据类别,通过分析,在类com.provider.ContactsContract.CommonDataKinds中查找到了每一个类别拥有的字段。

mimetype表中数据有:

id

mimetype

描述

1

vnd.android.cursor.item/email_v2

邮件

2

vnd.android.cursor.item/im

即时消息

3

vnd.android.cursor.item/nickname

昵称

4

vnd.android.cursor.item/organization

组织机构

5

vnd.android.cursor.item/phone_v2

电话

6

vnd.android.cursor.item/sip_address

sip地址

7

vnd.android.cursor.item/name

名字

8

vnd.android.cursor.item/postal-address_v2

邮政地址

9

vnd.android.cursor.item/identity

身份

10

vnd.android.cursor.item/photo

照片

11

vnd.android.cursor.item/group_membership

群组成员关系

12

vnd.android.cursor.item/note

备注

13

vnd.android.cursor.item/contact_event

联系事件

14

vnd.android.cursor.item/website

网站

15

vnd.android.cursor.item/relation

关系

16

vnd.android.cursor.item/contact_misc

杂项

有了这些表之间的关系,我们就可以写出来查询联系人的SQL语句:

SELECT t1.name_raw_contact_id, t2.display_name, t3.raw_contact_id, t3.mimetype_id, t4.mimetype, t3.data1, t3.data2, t3.data3, t3.data4
FROM contacts t1, raw_contacts t2, data t3, mimetypes t4 
WHERE t1.name_raw_contact_id = t2._id 
AND t3.raw_contact_id = t2._id 
AND t3.mimetype_id = t4._id 
AND t2.contact_id = t1._id
ORDER BY t1.name_raw_contact_id

执行SQL语句,得到数据如下: 

修改后再查询

 

 

读取联系人:

① 读取contacts表(t1),获取name_raw_contact_id字段;

② 在raw_contacts表(t2)中,根据name_raw_contact_id和_id对应,获取display_name字段;

③ 在data表(t3)中,根据raw_contact_id,获取该联系人的各数据,例如:data1,mimetype_id;

④ 在mimetypes表(t4)中,根据mimetype_id,获取mimetype。

新建联系人:

① 新建联系人时,根据contacts、raw_contacts两张表中ID的使用情况,自动生成name_raw_contact_id和raw_contact_id;

② Android原生系统,新建重复姓名的联系人的name_raw_contact_id是不重复的;但是有的厂商的Android系统的联系人程序在新建联系人,如果多次新建的联系人的姓名是一样的,生成的name_raw_contact_id也会重复,但raw_contact_id不会重复,我们在读取联系人的时候可以获取所有同姓名联系人的号码等信息,在显示联系人的时候,重复姓名的联系人的所有字段信息都会合并起来显示为一个联系人。

删除联系人:

将raw_contacts表中deleted字段设置为1,清空contact_id字段。

contacts表中与之对应的_id字段的行被自动删除(外键关联)。

data表中对应raw_contact_id字段的数据还在。

更新联系人:

联系人的所有信息都是保存在data表中,所以要更新联系人,我们只需要根据raw_contact_id和mimetype_id修改data表中的内容。

(3)查看手机短信

查阅资料得知,Android系统短信内容提供者的URI为“content://sms/”。

然后,我们再来了解一下系统短信的数据库文件,在DDMS窗口的File Explorer中/data/data/com.android.providers.telephony/database目录下找到mmssms.db文件,如图所示。

 

由于我们的模拟器没有短信记录,所以我们先给我们的模拟器发送几条短信用于测试。

打开Extended controls对话框,进入Phone界面,输入要发送短信的手机号码,编辑短信,点击“SEND MESSAGE”。

 

接收短信并查看短信内容。

 

然后,我们将数据库文件导出查看,其中sms表就是短信表。

 

其中,_id是短信的主键,address是发送或者接收短信的手机号,date是long类型的时间戳,type是短信类型(1代表接收到的短信,2代表发送出去的短信),body是短信内容。了解了sms表后,我们接下来编写程序实现查看系统短信的功能。

创建一个名为ReadSMS的应用程序,指定包名为com.sdbi.readsms。修改activity_main.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<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/lvSms"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></ListView>
</LinearLayout>

创建短信实体类SmsInfo,表示短信对象:

public class SmsInfo {
    private int _id;
    private String address;
    private long date;
    private int type;
    private String body;

    public SmsInfo(int _id, String address, long date, int type, String body) {
        this._id = _id;
        this.address = address;
        this.date = date;
        this.type = type;
        this.body = body;
    }

    public int get_id() {
        return _id;
    }

    public String getAddress() {
        return address;
    }

    public long getDate() {
        return date;
    }

    public int getType() {
        return type;
    }

    public String getBody() {
        return body;
    }
}

然后建立一个短信显示的布局,在layout目录下新建sms_item.xml,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:id="@+id/sms_address"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:textSize="20sp" />

    <TextView
        android:id="@+id/sms_body"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        android:textSize="16sp" />
</LinearLayout>

接下来,需要创建一个自定义的适配器,这个适配器继承自ArrayAdapter,并将泛型指定为SmsInfo类。新建类SmsInfoAdapter,代码如下:

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.util.List;

public class SmsInfoAdapter extends ArrayAdapter<SmsInfo> {
    private int resourceId;

    public SmsInfoAdapter(@NonNull Context context, int resource, @NonNull List<SmsInfo> objects) {
        super(context, resource, objects);
        resourceId = resource;
    }

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        View view;
        ViewHolder viewHolder;
        SmsInfo smsInfo = getItem(position); // 获取当前项的SmsInfo实例
        if (convertView == null) {
            LayoutInflater inflater = LayoutInflater.from(getContext());
            view = inflater.inflate(resourceId, null);
            viewHolder = new ViewHolder();
            viewHolder.smsAddress = (TextView) view.findViewById(R.id.sms_address);
            viewHolder.smsBody = (TextView) view.findViewById(R.id.sms_body);
            view.setTag(viewHolder);
        } else {
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();
        }
        viewHolder.smsAddress.setText("号码:" + smsInfo.getAddress());
        viewHolder.smsBody.setText("内容:" + smsInfo.getBody());
        return view;
    }

    // 内部类
    class ViewHolder {
        TextView smsAddress;
        TextView smsBody;
    }

}

下面修改MainActivity.java文件,代码如下:

import androidx.appcompat.app.AppCompatActivity;

import android.Manifest;
import android.content.ContentResolver;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.widget.ListView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    List<SmsInfo> lstSmsInfos = new ArrayList<SmsInfo>();
    private ListView lvSms;

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

        // 动态权限获取
        if (checkSelfPermission(Manifest.permission.READ_SMS) != PackageManager.PERMISSION_GRANTED) {
            requestPermissions(new String[]{Manifest.permission.READ_SMS},1);
        }

        readSms(); // 调用读取信息的方法
        SmsInfoAdapter adapter = new SmsInfoAdapter(this, R.layout.sms_item, lstSmsInfos);
        lvSms = (ListView) findViewById(R.id.lvSms);
        lvSms.setAdapter(adapter);
    }

    // 定义读取信息的方法
    public void readSms() {
        Uri uri = Uri.parse("content://sms/");
        ContentResolver resolver = getContentResolver();
        Cursor cursor = resolver.query(uri, new String[]{"_id", "address", "date", "type", "body"}, null, null, null);
        if (cursor != null && cursor.getCount() > 0) {
            while (cursor.moveToNext()) {
                int _id = cursor.getInt(0);
                String address = cursor.getString(1);
                long date = cursor.getLong(2);
                int type = cursor.getInt(3);
                String body = cursor.getString(4);
                SmsInfo smsInfo = new SmsInfo(_id, address, date, type, body);
                lstSmsInfos.add(smsInfo);
            }
            cursor.close();
        }
    }
}

最后,在AndroidManifest.xml清单文件中添加读取短信的权限:

<uses-permission android:name="android.permission.READ_SMS" />

运行程序,效果如图所示。和系统自带的短信软件显示很像。

    

 

posted @ 2022-12-06 09:37  熊猫Panda先生  阅读(917)  评论(0编辑  收藏  举报