ContentProvider的相关知识总结

news2024/9/21 5:30:13

1.ContentProvider概念讲解:

在这里插入图片描述

2.使用系统提供的ContentProvider

其实很多时候我们用到ContentProvider并不是自己暴露自己的数据,更多的时候通过 ContentResolver来读取其他应用的信息,最常用的莫过于读取系统APP,信息,联系人, 多媒体信息等!如果你想来调用这些ContentProvider就需要自行查阅相关的API资料了! 另外,不同的版本,可能对应着不同的URL!这里给出如何获取URL与对应的数据库表的字段, 这里以最常用的联系人为例,其他自行google~
①来到系统源码文件下:all-src.rar -> TeleponeProvider -> AndroidManifest.xml查找对应API
②打开模拟器的file exploer/data/data/com.android.providers.contacts/databases/contact2.db 导出后使用SQLite图形工具查看,三个核心的表:raw_contact表,data表,mimetypes表!
下面演示一些基本的操作示例:

1)简单的读取收件箱信息:

核心代码:

private void getMsgs(){
    Uri uri = Uri.parse("content://sms/");
    ContentResolver resolver = getContentResolver();
    //获取的是哪些列的信息
    Cursor cursor = resolver.query(uri, new String[]{"address","date","type","body"}, null, null, null);
    while(cursor.moveToNext())
    {
        String address = cursor.getString(0);
        String date = cursor.getString(1);
        String type = cursor.getString(2);
        String body = cursor.getString(3);
        System.out.println("地址:" + address);
        System.out.println("时间:" + date);
        System.out.println("类型:" + type);
        System.out.println("内容:" + body);
        System.out.println("======================");
    }
    cursor.close();
}

别忘了,往AndroidManifest.xml加入读取收件箱的权限:

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

运行结果:
部分运行结果如下:
在这里插入图片描述

2)简单的往收件箱里插入一条信息

核心代码:

private void insertMsg() {
    ContentResolver resolver = getContentResolver();
    Uri uri = Uri.parse("content://sms/");
    ContentValues conValues = new ContentValues();
    conValues.put("address", "123456789");
    conValues.put("type", 1);
    conValues.put("date", System.currentTimeMillis());
    conValues.put("body", "no zuo no die why you try!");
    resolver.insert(uri, conValues);
    Log.e("HeHe", "短信插入完毕~");
}

在这里插入图片描述
注意事项:
上述代码在4.4以下都可以实现写入短信的功能,而5.0上就无法写入,原因是: 从5.0开始,默认短信应用外的软件不能以写入短信数据库的形式发短信!

3)简单的读取手机联系人

核心代码:

private void getContacts(){
    //①查询raw_contacts表获得联系人的id
    ContentResolver resolver = getContentResolver();
    Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
    //查询联系人数据
    cursor = resolver.query(uri, null, null, null, null);
    while(cursor.moveToNext())
    {
        //获取联系人姓名,手机号码
        String cName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
        String cNum = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
        System.out.println("姓名:" + cName);
        System.out.println("号码:" + cNum);
        System.out.println("======================");
    }
    cursor.close();
}

别忘了加读联系人的权限:

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

运行结果:
部分运行结果如下:
在这里插入图片描述

4)查询指定电话的联系人信息

核心代码:

private void queryContact(String number){
        Uri uri = Uri.parse("content://com.android.contacts/data/phones/filter/" + number);
        ContentResolver resolver = getContentResolver();
        Cursor cursor = resolver.query(uri, new String[]{"display_name"}, null, null, null);
        if (cursor.moveToFirst()) {
            String name = cursor.getString(0);
            System.out.println(number + "对应的联系人名称:" + name);
        }
    cursor.close();
}

在这里插入图片描述

5)添加一个新的联系人

核心代码:

private void AddContact() throws RemoteException, OperationApplicationException {
    //使用事务添加联系人
    Uri uri = Uri.parse("content://com.android.contacts/raw_contacts");
    Uri dataUri =  Uri.parse("content://com.android.contacts/data");

    ContentResolver resolver = 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", "Coder-pig")
            .build();
    operations.add(op2);

    ContentProviderOperation op3 = ContentProviderOperation.newInsert(dataUri)
            .withValueBackReference("raw_contact_id", 0)
            .withValue("mimetype", "vnd.android.cursor.item/phone_v2")
            .withValue("data1", "13798988888")
            .withValue("data2", "2")
            .build();
    operations.add(op3);

    ContentProviderOperation op4 = ContentProviderOperation.newInsert(dataUri)
            .withValueBackReference("raw_contact_id", 0)
            .withValue("mimetype", "vnd.android.cursor.item/email_v2")
            .withValue("data1", "779878443@qq.com")
            .withValue("data2", "2")
            .build();
    operations.add(op4);
    //将上述内容添加到手机联系人中~
    resolver.applyBatch("com.android.contacts", operations);
    Toast.makeText(getApplicationContext(), "添加成功", Toast.LENGTH_SHORT).show();
}

在这里插入图片描述
别忘了权限:

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

3.自定义ContentProvider

我们很少会自己来定义ContentProvider,因为我们很多时候都不希望自己应用的数据暴露给 其他应用,虽然这样,学习如何ContentProvider还是有必要的,多一种数据传输的方式,是吧~
这是之前画的一个流程图:
在这里插入图片描述
接下来我们就来一步步实现:
在开始之前我们先要创建一个数据库创建类(数据库内容后面会讲~):
DBOpenHelper.java

public class DBOpenHelper extends SQLiteOpenHelper {

    final String CREATE_SQL = "CREATE TABLE test(_id INTEGER PRIMARY KEY AUTOINCREMENT,name)";
    
    public DBOpenHelper(Context context, String name, CursorFactory factory,
            int version) {
        super(context, name, null, 1);
    }

    
    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_SQL);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // TODO Auto-generated method stub

    }

}

Step 1:自定义ContentProvider类,实现onCreate(),getType(),根据需求重写对应的增删改查方法:
NameContentProvider.java

public class NameContentProvider extends ContentProvider {

    //初始化一些常量
     private static UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);        
     private DBOpenHelper dbOpenHelper;
    
    //为了方便直接使用UriMatcher,这里addURI,下面再调用Matcher进行匹配
     
     static{  
         matcher.addURI("com.jay.example.providers.myprovider", "test", 1);
     }  
     
    @Override
    public boolean onCreate() {
        dbOpenHelper = new DBOpenHelper(this.getContext(), "test.db", null, 1);
        return true;
    }

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

    @Override
    public String getType(Uri uri) {
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        
        switch(matcher.match(uri))
        {
        //把数据库打开放到里面是想证明uri匹配完成
        case 1:
            SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
            long rowId = db.insert("test", null, values);
            if(rowId > 0)
            {
                //在前面已有的Uri后面追加ID
                Uri nameUri = ContentUris.withAppendedId(uri, rowId);
                //通知数据已经发生改变
                getContext().getContentResolver().notifyChange(nameUri, null);
                return nameUri;
            }
        }
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

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

}

Step 2:AndroidManifest.xml中为ContentProvider进行注册:

<!--属性依次为:全限定类名,用于匹配的URI,是否共享数据 -->
<provider android:name="com.jay.example.bean.NameContentProvider"
            android:authorities="com.jay.example.providers.myprovider"
            android:exported="true" />

好的,作为ContentProvider的部分就完成了!

接下来,创建一个新的项目,我们来实现ContentResolver的部分,我们直接通过按钮点击插入一条数据:
MainActivity.java

public class MainActivity extends Activity {

    private Button btninsert;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        btninsert = (Button) findViewById(R.id.btninsert);
        
        //读取contentprovider 数据  
        final ContentResolver resolver = this.getContentResolver();
        
        
        btninsert.setOnClickListener(new OnClickListener() {
            
            @Override
            public void onClick(View v) {
                 ContentValues values = new ContentValues();
                 values.put("name", "测试");
                 Uri uri = Uri.parse("content://com.jay.example.providers.myprovider/test");
                resolver.insert(uri, values);
                Toast.makeText(getApplicationContext(), "数据插入成功", Toast.LENGTH_SHORT).show();
                
            }
        });
       
    }
}

如何使用? 好吧,代码还是蛮简单的,先运行作为ContentProvider的项目,接着再运行ContentResolver的项目, 点击按钮插入一条数据,然后打开file exploer将ContentProvider的db数据库取出,用图形查看工具 查看即可发现插入数据,时间关系,就不演示结果了~

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

在这里插入图片描述
使用指南:
运行程序后,晾一边,收到短信后,可以在logcat上看到该条信息的内容,可以根据自己的需求 将Activtiy改做Service,而在后台做这种事情~

5.简单走下文档:

Calendar Provider:日历提供者,就是针对针对日历相关事件的一个资源库,通过他提供的API,我们 可以对日历,时间,会议,提醒等内容做一些增删改查!
Contacts Provider:联系人提供者,这个就不用说了,这个用得最多~后面有时间再回头翻译下这篇文章吧!
Storage Access Framework(SAF):存储访问框架,4.4以后引入的一个新玩意,为用户浏览手机中的 存储内容提供了便利,可供访问的内容不仅包括:文档,图片,视频,音频,下载,而且包含所有由 由特定ContentProvider(须具有约定的API)提供的内容。不管这些内容来自于哪里,不管是哪个应 用调用浏览系统文件内容的命令,系统都会用一个统一的界面让你去浏览。
其实就是一个内置的应用程序,叫做DocumentsUI,因为它的IntentFilter不带有LAUNCHER,所以我们并没有 在桌面上找到这个东东!嘿嘿,试下下面的代码,这里我们选了两个手机来对比: 分别是4.2的Lenovo S898T 和 5.0.1的Nexus 5做对比,执行下述代码:

 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        startActivity(intent);

在这里插入图片描述
在这里插入图片描述
右面这个就是4.4给我们带来的新玩意了,一般我们获取文件Url的时候就可以用到它~ 接下来简单的走下文档吧~

1)SAF框架的组成:

Document provider:一个特殊的ContentProvider,让一个存储服务(比如Google Drive)可以 对外展示自己所管理的文件。它是DocumentsProvider的子类,另外,document-provider的存储格式 和传统的文件存储格式一致,至于你的内容如何存储,则完全决定于你自己,Android系统已经内置了几个 这样的Document provider,比如关于下载,图片以及视频的Document provider!
Client app:一个普通的客户端软件,通过触发ACTION_OPEN_DOCUMENT 和/或 ACTION_CREATE_DOCUMENT就可以接收到来自于Document provider返回的内容,比如选择一个图片, 然后返回一个Uri。
Picker:类似于文件管理器的界面,而且是系统级的界面,提供额访问客户端过滤条件的 Document provider内容的通道,就是起说的那个DocumentsUI程序!

一些特性:
用户可以浏览所有document provider提供的内容,而不仅仅是单一的应用程序
提供了长期、持续的访问document provider中文件的能力以及数据的持久化, 用户可以实现添加、删除、编辑、保存document provider所维护的内容
支持多用户以及临时性的内容服务,比如USB storage providers只有当驱动安装成功才会出现

2)概述:

SAF的核心是实现了DocumentsProvider的子类,还是一个ContentProvider。在一个document provider 中是以传统的文件目录树组织起来的:
在这里插入图片描述

3)流程图:

如上面所述,document provider data是基于传统的文件层次结构的,不过那只是对外的表现形式, 如何存储你的数据,取决于你自己,只要你对海外的接口能够通过DocumentsProvider的api访问就可以。 下面的流程图展示了一个photo应用使用SAF可能的结构:
在这里插入图片描述
分析:
从上图,我们可以看出Picker是链接调用者和内容提供者的一个桥梁!他提供并告诉调用者,可以选择 哪些内容提供者,比如这里的DriveDocProvider,UsbDocProvider,CloundDocProvider。

当客户端触发了ACTION_OPEN_DOCUMENT或ACTION_CREATE_DOCUMENT的Intent,就会发生上述交互。 当然我们还可以在Intent中增加过滤条件,比如限制MIME type的类型为"image"!
在这里插入图片描述
就是上面这些东西,如果你还安装了其他看图的软件的话,也会在这里看到! 简单点说就是:客户端发送了上面两种Action的Intent后,会打开Picker UI,在这里会显示相关可用的 Document Provider,供用户选择,用户选择后可以获得文件的相关信息!

4)客户端调用,并获取返回的Uri

实现代码如下:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final int READ_REQUEST_CODE = 42;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn_show = (Button) findViewById(R.id.btn_show);
        btn_show.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        startActivityForResult(intent, READ_REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            Uri uri;
            if (data != null) {
                uri = data.getData();
                Log.e("HeHe", "Uri: " + uri.toString());
            }
        }
    }
}

运行结果: 比如我们选中那只狗,然后Picker UI自己会关掉,然后Logcat上可以看到这样一个uri:
在这里插入图片描述

5)根据uri获取文件参数

核心代码如下:

public void dumpImageMetaData(Uri uri) {
    Cursor cursor = getContentResolver()
            .query(uri, null, null, null, null, null);
    try {
        if (cursor != null && cursor.moveToFirst()) {
            String displayName = cursor.getString(
                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            Log.e("HeHe", "Display Name: " + displayName);
            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
            String size = null;
            if (!cursor.isNull(sizeIndex)) {
                size = cursor.getString(sizeIndex);
            }else {
                size = "Unknown";
            }
            Log.e("HeHe", "Size: " + size);
        }
    }finally {
        cursor.close();
    }
}

运行结果: 还是那只狗,调用方法后会输入文件名以及文件大小,以byte为单位
在这里插入图片描述

6)根据Uri获得Bitmap

核心代码如下:

private Bitmap getBitmapFromUri(Uri uri) throws IOException {
        ParcelFileDescriptor parcelFileDescriptor =
        getContentResolver().openFileDescriptor(uri, "r");
        FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
        Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
        parcelFileDescriptor.close();
        return image;
}

在这里插入图片描述

7)根据Uri获取输入流

核心代码如下:

private String readTextFromUri(Uri uri) throws IOException {
    InputStream inputStream = getContentResolver().openInputStream(uri);
    BufferedReader reader = new BufferedReader(new InputStreamReader(
            inputStream));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        stringBuilder.append(line);
    }
    fileInputStream.close();
    parcelFileDescriptor.close();
    return stringBuilder.toString();
}

上述的内容只告诉你通过一个Uri你可以知道什么,而Uri的获取则是通过SAF得到的!

8) 创建新文件以及删除文件:

创建文件:

private void createFile(String mimeType, String fileName) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType(mimeType);
    intent.putExtra(Intent.EXTRA_TITLE, fileName);
    startActivityForResult(intent, WRITE_REQUEST_CODE);
}

可在onActivityResult()中获取被创建文件的uri
删除文件:
前提是Document.COLUMN_FLAGS包含SUPPORTS_DELETE

DocumentsContract.deleteDocument(getContentResolver(), uri);

9)编写一个自定义的Document Provider

如果你希望自己应用的数据也能在documentsui中打开,你就需要写一个自己的document provider。 下面介绍自定义DocumentsProvider的步骤:

API版本为19或者更高
在manifest.xml中注册该Provider
Provider的name为类名加包名,比如: com.example.android.storageprovider.MyCloudProvider
Authority为包名+provider的类型名,如: com.example.android.storageprovider.documents
android:exported属性的值为ture

下面是Provider的例子写法:

<manifest... >
    ...
    <uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="19" />
        ....
        <provider
            android:name="com.example.android.storageprovider.MyCloudProvider"
            android:authorities="com.example.android.storageprovider.documents"
            android:grantUriPermissions="true"
            android:exported="true"
            android:permission="android.permission.MANAGE_DOCUMENTS"
            android:enabled="@bool/atLeastKitKat">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>

10 )DocumentsProvider的子类

至少实现如下几个方法:

queryRoots()
queryChildDocuments()
queryDocument()
openDocument()

还有些其他的方法,但并不是必须的。下面演示一个实现访问文件(file)系统的 DocumentsProvider的大致写法。
Implement queryRoots

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {

    // Create a cursor with either the requested fields, or the default
    // projection if "projection" is null.
    final MatrixCursor result =
            new MatrixCursor(resolveRootProjection(projection));

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    // Construct one row for a root called "MyCloud".
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);
    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));

    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
    // recently used documents will show up in the "Recents" category.
    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
    // shares.
    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
            Root.FLAG_SUPPORTS_RECENTS |
            Root.FLAG_SUPPORTS_SEARCH);

    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));

    // This document id cannot change once it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));

    // The child MIME types are used to filter the roots and only present to the
    //  user roots that contain the desired type somewhere in their file hierarchy.
    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir));
    row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);

    return result;
}

Implement queryChildDocuments

public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
                              String sortOrder) throws FileNotFoundException {

    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    final File parent = getFileForDocId(parentDocumentId);
    for (File file : parent.listFiles()) {
        // Adds the file's display name, MIME type, size, and so on.
        includeFile(result, null, file);
    }
    return result;
}

Implement queryDocument

@Override
public Cursor queryDocument(String documentId, String[] projection) throws
        FileNotFoundException {

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    includeFile(result, documentId, null);
    return result;
}

6.在应用之间共享数据

在这里插入图片描述
在这里插入图片描述

6.1通过ContentProvider封装数据

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

1.编写用户信息表的数据库帮助器

这个数据库帮助器就是常规的SQLite操作代码,实现过程参见上一章的“6.2.3 数据库帮助器SQLiteOpenHelper”

2.编写内容提供器的基础字段类

该类需要实现接口BaseColumns,同时加入几个常量定义。详细代码示例如下:

public class UserInfoContent implements BaseColumns {
// 这里的名称必须与AndroidManifest.xml里的android:authorities保持一致
public static final String AUTHORITIES =
"com.example.chapter07.provider.UserInfoProvider";
// 内容提供器的外部表名
public static final String TABLE_NAME = UserDBHelper.TABLE_NAME;
// 访问内容提供器的URI
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITIES +
"/user");
// 下面是该表的各个字段名称
public static final String USER_NAME = "name";
public static final String USER_AGE = "age";
public static final String USER_HEIGHT = "height";
public static final String USER_WEIGHT = "weight";
}

3.通过右键菜单创建内容提供器

右击App模块的包名目录,在弹出的右键菜单中依次选择New→Other→Content Provider,打开如图7-1所示的组件创建对话框。
在这里插入图片描述
在创建对话框的Class Name一栏填写内容提供器的名称,比如UserInfoProvider;在URI Authorities一栏填写URI的授权串,比如“com.example.chapter07.provider.UserInfoProvider”;然后单击对话框右下角的Finish按钮,完成提供器的创建操作。

上述创建过程会自动修改App模块的两处地方,一处是往AndroidManifest.xml添加内容提供器的注册配置,配置信息示例如下:

<!-- provider的authorities属性值需要与Java代码的AUTHORITIES保持一致 -->
<provider
android:name=".provider.UserInfoProvider"
android:authorities="com.example.chapter07.provider.UserInfoProvider"
android:enabled="true"
android:exported="true" />

另一处是在包名目录下生成名为UserInfoProvider.java的代码文件,打开一看发现该类继承了ContentProvider,并且提示重写onCreate、insert、delete、query、update、getType等方法,以便对数据进行增删改查等操作。这个提供器代码显然只有一个框架,还需补充详细的实现代码,为此重写onCreate方法,在此获取用户信息表的数据库帮助器实例,其他insert、delete、query等方法也要加入对应的数据库操作代码,修改之后的内容提供器代码如下所示:

public class UserInfoProvider extends ContentProvider {
private final static String TAG = "UserInfoProvider";
private UserDBHelper userDB; // 声明一个用户数据库的帮助器对象
public static final int USER_INFO = 1; // Uri匹配时的代号
public static final UriMatcher uriMatcher = new
UriMatcher(UriMatcher.NO_MATCH);
static { // 往Uri匹配器中添加指定的数据路径
uriMatcher.addURI(UserInfoContent.AUTHORITIES, "/user", USER_INFO);
}
// 创建ContentProvider时调用,可在此获取具体的数据库帮助器实例
@Override
public boolean onCreate() {
userDB = UserDBHelper.getInstance(getContext(), 1);
return true;
}
// 插入数据
@Override
public Uri insert(Uri uri, ContentValues values) {
if (uriMatcher.match(uri) == USER_INFO) { // 匹配到了用户信息表
// 获取SQLite数据库的写连接
SQLiteDatabase db = userDB.getWritableDatabase();
// 向指定的表插入数据,返回记录的行号
long rowId = db.insert(UserInfoContent.TABLE_NAME, null, values);
if (rowId > 0) { // 判断插入是否执行成功
// 如果添加成功,就利用新记录的行号生成新的地址
Uri newUri =
ContentUris.withAppendedId(UserInfoContent.CONTENT_URI, rowId);
// 通知监听器,数据已经改变
getContext().getContentResolver().notifyChange(newUri, null);
}
db.close(); // 关闭SQLite数据库连接
}
return uri;
}
// 根据指定条件删除数据
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
int count = 0;
if (uriMatcher.match(uri) == USER_INFO) { // 匹配到了用户信息表
// 获取SQLite数据库的写连接
SQLiteDatabase db = userDB.getWritableDatabase();
// 执行SQLite的删除操作,并返回删除记录的数目
count = db.delete(UserInfoContent.TABLE_NAME, selection,
selectionArgs);
db.close(); // 关闭SQLite数据库连接
}
return count;
}
// 根据指定条件查询数据库
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Cursor cursor = null;
if (uriMatcher.match(uri) == USER_INFO) { // 匹配到了用户信息表
// 获取SQLite数据库的读连接
SQLiteDatabase db = userDB.getReadableDatabase();
// 执行SQLite的查询操作
cursor = db.query(UserInfoContent.TABLE_NAME,
projection, selection, selectionArgs, null, null,
sortOrder);
// 设置内容解析器的监听
cursor.setNotificationUri(getContext().getContentResolver(), uri);
}
return cursor; // 返回查询结果集的游标
}
// 获取Uri支持的数据类型,暂未实现
@Override
public String getType(Uri uri) {
throw new UnsupportedOperationException("Not yet implemented");
}
// 更新数据,暂未实现
@Override
public int update(Uri uri, ContentValues values, String selection, String[]
selectionArgs) {
throw new UnsupportedOperationException("Not yet implemented");
}
}

经过以上3个步骤之后,便完成了服务端App的接口封装工作,接下来再由其他App去访问服务端App的数据。

6.2 通过ContentResolver访问数据

上一小节提到了利用ContentProvider封装服务端App的数据,如果客户端App想访问对方的内部数据,就要借助内容解析器ContentResolver。内容解析器是客户端App操作服务端数据的工具,与之对应的内容提供器则是服务端的数据接口。在活动代码中调用getContentResolver方法,即可获取内容解析器的实例。

ContentResolver提供的方法与ContentProvider一一对应,比如insert、delete、query、update、getType等,甚至连方法的参数类型都雷同。以添加操作为例,针对前面UserInfoProvider提供的数据接口,下面由内容解析器调用insert方法,使之往内容提供器插入一条用户信息,记录添加代码如下所示:

// 添加一条用户记录
private void addUser(UserInfo user) {
ContentValues name = new ContentValues();
name.put("name", user.name);
name.put("age", user.age);
name.put("height", user.height);
name.put("weight", user.weight);
name.put("married", 0);
name.put("update_time", DateUtil.getNowDateTime(""));
// 通过内容解析器往指定Uri添加用户信息
getContentResolver().insert(UserInfoContent.CONTENT_URI, name);
}

至于删除操作就更简单了,只要下面一行代码就删除了所有记录:

getContentResolver().delete(UserInfoContent.CONTENT_URI, "1=1", null);

查询操作稍微复杂一些,调用query方法会返回游标对象,这个游标正是SQLite的游标Cursor,详细用法参见上一章的“6.2.3 数据库帮助器SQLiteOpenHelper”。query方法的输入参数有好几个,具体说明如下(依参数顺序排列)。
uri:Uri类型,指定本次操作的数据表路径。
projection:字符串数组类型,指定将要查询的字段名称列表。
selection:字符串类型,指定查询条件。
selectionArgs:字符串数组类型,指定查询条件中的参数取值列表。
sortOrder:字符串类型,指定排序条件。

下面是调用query方法从内容提供器查询所有用户信息的代码例子:

// 显示所有的用户记录
private void showAllUser() {
List<UserInfo> userList = new ArrayList<UserInfo>();
// 通过内容解析器从指定Uri中获取用户记录的游标
Cursor cursor = getContentResolver().query(UserInfoContent.CONTENT_URI,
null, null, null, null);
// 循环取出游标指向的每条用户记录
while (cursor.moveToNext()) {
UserInfo user = new UserInfo();
user.name =
cursor.getString(cursor.getColumnIndex(UserInfoContent.USER_NAME));
user.age =
cursor.getInt(cursor.getColumnIndex(UserInfoContent.USER_AGE));
user.height =
cursor.getInt(cursor.getColumnIndex(UserInfoContent.USER_HEIGHT));
user.weight =
cursor.getFloat(cursor.getColumnIndex(UserInfoContent.USER_WEIGHT));
userList.add(user); // 添加到用户信息列表
}
cursor.close(); // 关闭数据库游标
String contactCount = String.format("当前共找到%d个用户", userList.size());
tv_desc.setText(contactCount);
ll_list.removeAllViews(); // 移除线性布局下面的所有下级视图
for (UserInfo user : userList) { // 遍历用户信息列表
String contactDesc = String.format("姓名为%s,年龄为%d,身高为%d,体重为%f\n",
user.name, user.age, user.height,
user.weight);
TextView tv_contact = new TextView(this); // 创建一个文本视图
tv_contact.setText(contactDesc);
tv_contact.setTextColor(Color.BLACK);
tv_contact.setTextSize(17);
int pad = Utils.dip2px(this, 5);
tv_contact.setPadding(pad, pad, pad, pad); // 设置文本视图的内部间距
ll_list.addView(tv_contact); // 把文本视图添加至线性布局
}
}

接下来分别演示通过内容解析器添加和查询用户信息的过程,其中记录添加页面为
ContentWriteActivity.java,记录查询页面为ContentReadActivity.java。运行测试App,先打开记录添加页面,输入用户信息后点击添加按钮,由内容解析器执行插入操作,此时添加界面如图7-2所示。接着打开记录查询页面,内容解析器自动执行查询操作,并将查到的用户信息一一显示出来,此时查询界面如图7-3所示。
在这里插入图片描述

7 使用内容组件获取通讯信息

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.1 运行时动态申请权限

上一章的“6.3.1 公共存储空间与私有存储空间”提到,App若想访问存储卡的公共空间,就要在AndroidManifest.xml里面添加下述的权限配置。

<!-- 存储卡读写 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAG" />

然而即使App声明了完整的存储卡操作权限,从Android 7.0开始,系统仍然默认禁止该App访问公共空间,必须到设置界面手动开启应用的存储卡权限才行。尽管此举是为用户隐私着想,可是人家咋知道要手工开权限呢?就算用户知道,去设置界面找到权限开关也颇费周折。为此Android支持在Java代码中处理权限,处理过程分为3个步骤,详述如下:

1.检查App是否开启了指定权限

权限检查需要调用ContextCompat的checkSelfPermission方法,该方法的第一个参数为活动实例,第二个参数为待检查的权限名称,例如存储卡的写权限名为
Manifest.permission.WRITE_EXTERNAL_STORAGE。注意checkSelfPermission方法的返回值,当它为PackageManager.PERMISSION_GRANTED时表示已经授权,否则就是未获授权。

2.请求系统弹窗,以便用户选择是否开启权限

一旦发现某个权限尚未开启,就得弹窗提示用户手工开启,这个弹窗不是开发者自己写的提醒对话框,而是系统专门用于权限申请的对话框。调用ActivityCompat的requestPermissions方法,即可命令系统自动弹出权限申请窗口,该方法的第一个参数为活动实例,第二个参数为待申请的权限名称数组,第三个参数为本次操作的请求代码。

3.判断用户的权限选择结果

然而上面第二步的requestPermissions方法没有返回值,那怎么判断用户到底选了开启权限还是拒绝权限呢?其实活动页面提供了权限选择的回调方法onRequestPermissionsResult,如果当前页面请求弹出权限申请窗口,那么该页面的Java代码必须重写onRequestPermissionsResult方法,并在该方法内部处理用户的权限选择结果。具体到编码实现上,前两步的权限校验和请求弹窗可以合并到一块,先调用checkSelfPermission方法检查某个权限是否已经开启,如果没有开启再调用requestPermissions方法请求系统弹窗。合并之后的检查方法代码示例如下,此处代码支持一次检查一个权限,也支持一次检查多个权限:

// 检查某个权限。返回true表示已启用该权限,返回false表示未启用该权限
public static boolean checkPermission(Activity act, String permission, int
requestCode) {
return checkPermission(act, new String[]{permission}, requestCode);
}
// 检查多个权限。返回true表示已完全启用权限,返回false表示未完全启用权限
public static boolean checkPermission(Activity act, String[] permissions, int
requestCode) {
boolean result = true;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
int check = PackageManager.PERMISSION_GRANTED;
// 通过权限数组检查是否都开启了这些权限
for (String permission : permissions) {
check = ContextCompat.checkSelfPermission(act, permission);
if (check != PackageManager.PERMISSION_GRANTED) {
break; // 有个权限没有开启,就跳出循环
}
}
if (check != PackageManager.PERMISSION_GRANTED) {
// 未开启该权限,则请求系统弹窗,好让用户选择是否立即开启权限
ActivityCompat.requestPermissions(act, permissions, requestCode);
result = false;
}
}
return result;
}

注意到上面代码有判断安卓版本号,只有系统版本大于Android 6.0(版本代号为M),才执行后续的权限校验操作。这是因为从Android 6.0开始引入了运行时权限机制,在Android 6.0之前,只要App在AndroidManifest.xml中添加了权限配置,则系统会自动给App开启相关权限;但在Android 6.0之后,即便事先添加了权限配置,系统也不会自动开启权限,而要开发者在App运行时判断权限的开关情况,再据此动态申请未获授权的权限。

回到活动页面代码,一方面增加权限校验入口,比如点击某个按钮后触发权限检查操作,其中Manifest.permission.WRITE_EXTERNAL_STORAGE表示存储卡权限,入口代码如下:

if (v.getId() == R.id.btn_file_write) {
if (PermissionUtil.checkPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE, R.id.btn_file_write % 65536)) {
startActivity(new Intent(this, FileWriteActivity.class));
}
}

另一方面还要重写活动的onRequestPermissionsResult方法,在方法内部校验用户的选择结果,若用户同意授权,就执行后续业务;若用户拒绝授权,只能提示用户无法开展后续业务了。重写后的方法代码如下所示:

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
// requestCode不能为负数,也不能大于2的16次方即65536
if (requestCode == R.id.btn_file_write % 65536) {
if (PermissionUtil.checkGrant(grantResults)) { // 用户选择了同意授权
startActivity(new Intent(this, FileWriteActivity.class));
} else {
ToastUtil.show(this, "需要允许存储卡权限才能写入公共空间噢");
}
}
}

以上代码为了简化逻辑,将结果校验操作封装为PermissionUtil的checkGrant方法,该方法遍历授权结果数组,依次检查每个权限是否都得到授权了。详细的方法代码如下所示:

// 检查权限结果数组,返回true表示都已经获得授权。返回false表示至少有一个未获得授权
public static boolean checkGrant(int[] grantResults) {
boolean result = true;
if (grantResults != null) {
for (int grant : grantResults) { // 遍历权限结果数组中的每条选择结果
if (grant != PackageManager.PERMISSION_GRANTED) { // 未获得授权
result = false;
}
}
} else {
result = false;
}
return result;
}

代码都改好后,运行测试App,由于一开始App默认未开启存储卡权限,因此点击按钮btn_file_write触发了权限校验操作,弹出如图7-4所示的存储卡权限申请窗口。
在这里插入图片描述
点击弹窗上的“始终允许”,表示同意赋予存储卡读写权限,然后系统自动给App开启了存储卡权限,并执行后续处理逻辑,也就是跳到了FileWriteActivity页面,在该页面即可访问公共空间的文件了。但在Android 10系统中,即使授权通过,App仍然无法访问公共空间,这是因为Android 10默认开启沙箱模式,不允许直接使用公共空间的文件路径,此时要修改AndroidManifest.xml,给application节点添加如下requestLegacyExternalStorage属性:
android:requestLegacyExternalStorage=“true”

从Android 11开始,为了让应用升级时也能正常访问公共空间,还得修改AndroidManifest.xml,给application节点添加如下的preserveLegacyExternalStorage属性,表示暂时关闭沙箱模式:
android:preserveLegacyExternalStorage=“true”

除了存储卡的读写权限,还有部分权限也要求运行时动态申请,这些权限名称的取值说明见表7-1。
在这里插入图片描述

7.2 利用ContentResolver读写联系人

在实际开发中,普通App很少会开放数据接口给其他应用访问,作为服务端接口的ContentProvider基本用不到。内容组件能够派上用场的情况,往往是App想要访问系统应用的通讯数据,比如查看联系人、短信、通话记录,以及对这些通讯数据进行增、删、改、查。

访问系统的通讯数据之前,得先在AndroidManifest.xml添加相应的权限配置,常见的通讯权限配置主要有下面几个:

<!-- 联系人/通讯录。包括读联系人、写联系人 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<!-- 短信。包括发送短信、接收短信、读短信-->
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<!-- 通话记录。包括读通话记录、写通话记录 -->
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />

当然,从Android 6.0开始,上述的通讯权限默认是关闭的,必须在运行App的时候动态申请相关权限,详细的权限申请过程参见上一小节的“7.2.1 运行时动态申请权限”。

尽管系统允许App通过内容解析器修改联系人列表,但操作过程比较烦琐,因为一个联系人可能有多个电话号码,还可能有多个邮箱,所以系统通讯录将其设计为3张表,分别是联系人基本信息表、联系号码表、联系邮箱表,于是每添加一位联系人,就要调用至少三次insert方法。下面是往手机通讯录添加联系人信息的代码例子:

// 往手机通讯录添加一个联系人信息(包括姓名、电话号码、电子邮箱)
public static void addContacts(ContentResolver resolver, Contact contact) {
// 构建一个指向系统联系人提供器的Uri对象
Uri raw_uri = Uri.parse("content://com.android.contacts/raw_contacts");
ContentValues values = new ContentValues(); // 创建新的配对
// 往 raw_contacts 添加联系人记录,并获取添加后的联系人编号
long contactId = ContentUris.parseId(resolver.insert(raw_uri, values));
// 构建一个指向系统联系人数据的Uri对象
Uri uri = Uri.parse("content://com.android.contacts/data");
ContentValues name = new ContentValues(); // 创建新的配对
name.put("raw_contact_id", contactId); // 往配对添加联系人编号
// 往配对添加“姓名”的数据类型
name.put("mimetype", "vnd.android.cursor.item/name");
name.put("data2", contact.name); // 往配对添加联系人的姓名
resolver.insert(uri, name); // 往提供器添加联系人的姓名记录
ContentValues phone = new ContentValues(); // 创建新的配对
phone.put("raw_contact_id", contactId); // 往配对添加联系人编号
// 往配对添加“电话号码”的数据类型
phone.put("mimetype", "vnd.android.cursor.item/phone_v2");
phone.put("data1", contact.phone); // 往配对添加联系人的电话号码
phone.put("data2", "2"); // 联系类型。1表示家庭,2表示工作
resolver.insert(uri, phone); // 往提供器添加联系人的号码记录
ContentValues email = new ContentValues(); // 创建新的配对
email.put("raw_contact_id", contactId); // 往配对添加联系人编号
// 往配对添加“电子邮箱”的数据类型
email.put("mimetype", "vnd.android.cursor.item/email_v2");
email.put("data1", contact.email); // 往配对添加联系人的电子邮箱
email.put("data2", "2"); // 联系类型。1表示家庭,2表示工作
resolver.insert(uri, email); // 往提供器添加联系人的邮箱记录
}

同理,联系人读取代码也分成3个步骤,先查出联系人的基本信息,再依次查询联系人号码和联系人邮箱,详细代码参见CommunicationUtil.java的readAllContacts方法。

接下来演示联系人信息的访问过程,分别创建联系人的添加页面和查询页面,其中添加页面的完整代码
见chapter07\src\main\java\com\example\chapter07\ContactAddActivity.java,查询页面的完整代码
见chapter07\src\main\java\com\example\chapter07\ContactReadActivity.java。首先在添加页面输入联系人信息,点击添加按钮调用addContacts方法写入联系人数据,此时添加界面如图7-5所示。然后打开联系人查询页面,App自动调用readAllContacts方法查出所有的联系人,并显示联系人列表如图7-6所示,可见刚才添加的联系人已经成功写入系统的联系人列表,而且也能正确读取最新的联系人信息。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.3 利用ContentObserver监听短信

ContentResolver获取数据采用的是主动查询方式,有查询就有数据,没查询就没数据。然而有时不但要获取以往的数据,还要实时获取新增的数据,最常见的业务场景是短信验证码。电商App经常在用户注册或付款时发送验证码短信,为了替用户省事,App通常会监控手机刚收到的短信验证码,并自动填写验证码输入框。这时就用到了内容观察器ContentObserver,事先给目标内容注册一个观察器,目标内容的数据一旦发生变化,就马上触发观察器的监听事件,从而执行开发者预先定义的代码。

内容观察器的用法与内容提供器类似,也要从ContentObserver派生一个新的观察器,然后通过ContentResolver对象调用相应的方法注册或注销观察器。下面是内容解析器与内容观察器之间的交互方法说明。
registerContentObserver:内容解析器要注册内容观察器。
unregisterContentObserver:内容解析器要注销内容观察器。
notifyChange:通知内容观察器发生了数据变化,此时会触发观察器的onChange方法。
notifyChange的调用时机参见“7.1.1 通过ContentProvider封装数据”的insert代码。

为了让读者更好理解,下面举一个实际应用的例子。手机号码的每月流量限额由移动运营商指定,以中国移动为例,只要将流量校准短信发给运营商客服号码(如发送18到10086),运营商就会回复用户本月的流量数据,包括月流量额度、已使用流量、未使用流量等信息。手机App只需监控10086发来的短信内容,即可自动获取当前号码的流量详情。下面是利用内容观察器实现流量校准的关键代码片段:

public class MonitorSmsActivity extends AppCompatActivity implements
View.OnClickListener {
private static final String TAG = "MonitorSmsActivity";
private static TextView tv_check_flow;
private static String mCheckResult;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_monitor_sms);
tv_check_flow = findViewById(R.id.tv_check_flow);
tv_check_flow.setOnClickListener(this);
findViewById(R.id.btn_check_flow).setOnClickListener(this);
initSmsObserver();
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn_check_flow) {
//查询数据流量,移动号码的查询方式为发送短信内容“18”给“10086”
//电信和联通号码的短信查询方式请咨询当地运营商客服热线
//跳到系统的短信发送页面,由用户手工发短信
//sendSmsManual("10086", "18");
//无需用户操作,自动发送短信
sendSmsAuto("10086", "18");
} else if (v.getId() == R.id.tv_check_flow) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("收到流量校准短信");
builder.setMessage(mCheckResult);
builder.setPositiveButton("确定", null);
builder.create().show();
}
}
// 跳到系统的短信发送页面,由用户手工编辑与发送短信
public void sendSmsManual(String phoneNumber, String message) {
Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:" +
phoneNumber));
intent.putExtra("sms_body", message);
startActivity(intent);
}
// 短信发送事件
private String SENT_SMS_ACTION = "com.example.storage.SENT_SMS_ACTION";
// 短信接收事件
private String DELIVERED_SMS_ACTION =
"com.example.storage.DELIVERED_SMS_ACTION";
// 无需用户操作,由App自动发送短信
public void sendSmsAuto(String phoneNumber, String message) {
// 以下指定短信发送事件的详细信息
Intent sentIntent = new Intent(SENT_SMS_ACTION);
sentIntent.putExtra("phone", phoneNumber);
sentIntent.putExtra("message", message);
PendingIntent sentPI = PendingIntent.getBroadcast(this, 0,
sentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// 以下指定短信接收事件的详细信息
Intent deliverIntent = new Intent(DELIVERED_SMS_ACTION);
deliverIntent.putExtra("phone", phoneNumber);
deliverIntent.putExtra("message", message);
PendingIntent deliverPI = PendingIntent.getBroadcast(this, 1,
deliverIntent, PendingIntent.FLAG_UPDATE_CURRENT);
// 获取默认的短信管理器
SmsManager smsManager = SmsManager.getDefault();
// 开始发送短信内容。要确保打开发送短信的完全权限,不是那种还需提示的不完整权限
smsManager.sendTextMessage(phoneNumber, null, message, sentPI,
deliverPI);
}
private Handler mHandler = new Handler(Looper.myLooper()); // 声明一个处理器对象
private SmsGetObserver mObserver; // 声明一个短信获取的观察器对象
private static Uri mSmsUri; // 声明一个系统短信提供器的Uri对象
private static String[] mSmsColumn; // 声明一个短信记录的字段数组
// 初始化短信观察器
private void initSmsObserver() {
//mSmsUri = Uri.parse("content://sms/inbox");
//Android5.0之后似乎无法单独观察某个信箱,只能监控整个短信
mSmsUri = Uri.parse("content://sms"); // 短信数据的提供器路径
mSmsColumn = new String[]{"address", "body", "date"}; // 短信记录的字段数组
// 创建一个短信观察器对象
mObserver = new SmsGetObserver(this, mHandler);
// 给指定Uri注册内容观察器,一旦发生数据变化,就触发观察器的onChange方法
getContentResolver().registerContentObserver(mSmsUri, true, mObserver);
}
// 在页面销毁时触发
protected void onDestroy() {
super.onDestroy();
getContentResolver().unregisterContentObserver(mObserver); // 注销内容观察}
// 定义一个短信获取的观察器
private static class SmsGetObserver extends ContentObserver {
private Context mContext; // 声明一个上下文对象
public SmsGetObserver(Context context, Handler handler) {
super(handler);
mContext = context;
}
// 观察到短信的内容提供器发生变化时触发
public void onChange(boolean selfChange) {
String sender = "", content = "";
// 构建一个查询短信的条件语句,移动号码要查找10086发来的短信
String selection = String.format("address='10086' and date>%d",
System.currentTimeMillis() - 1000 * 60 * 1); // 查找最近一分钟
的短信
// 通过内容解析器获取符合条件的结果集游标
Cursor cursor = mContext.getContentResolver().query(
mSmsUri, mSmsColumn, selection, null, " date desc");
// 循环取出游标所指向的所有短信记录
while (cursor.moveToNext()) {
sender = cursor.getString(0); // 短信的发送号码
content = cursor.getString(1); // 短信内容
Log.d(TAG, "sender="+sender+", content="+content);
break;
}
cursor.close(); // 关闭数据库游标
mCheckResult = String.format("发送号码:%s\n短信内容:%s", sender,
content);
// 依次解析流量校准短信里面的各项流量数值,并拼接流量校准的结果字符串
String flow = String.format("流量校准结果如下:总流量为:%s;已使用:%s" +
";剩余流量:%s", findFlow(content, "总流量为"),
findFlow(content, "已使用"), findFlow(content, "剩余"));
if (tv_check_flow != null) { // 离开该页面后就不再显示流量信息
tv_check_flow.setText(flow); // 在文本视图显示流量校准结果
}
super.onChange(selfChange);
}
}
// 解析流量短信里面的流量数值
private static String findFlow(String sms, String begin) {
String flow = findString(sms, begin, "GB");
String temp = flow.replace("GB", "").replace(".", "");
if (!temp.matches("\\d+")) {
flow = findString(sms, begin, "MB");
}
return flow;
}
// 截取指定头尾之间的字符串
private static String findString(String content, String begin, String end) {
int begin_pos = content.indexOf(begin);
if (begin_pos < 0) {
return "未获取";
}
String sub_sms = content.substring(begin_pos);
int end_pos = sub_sms.indexOf(end);
if (end_pos < 0) {
return "未获取";
}
if (end.equals(",")) {
return sub_sms.substring(begin.length(), end_pos);
} else {
return sub_sms.substring(begin.length(), end_pos + end.length());
}
}
}

运行测试App,点击校准按钮发送流量校准短信,接着收到如图7-7所示的短信内容。同时App监听刚收到的流量短信,从中解析得到当前的流量数值,并展示在界面上如图7-8所示。可见通过内容观察器实时获取了最新的短信记录。
在这里插入图片描述
在这里插入图片描述

8.在应用之间共享文件

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.1使用相册图片发送彩信

不同应用之间可以共享数据,当然也能共享文件,比如系统相册保存着用户拍摄的照片,这些照片理应分享给其他App使用。举个例子,短信只能发送文本,而彩信允许同时发送文本和图片,彩信的附件图片就来自系统相册。现在准备到系统相册挑选照片,测试页面的Java代码先增加以下两行代码,分别声明一个路径对象和选择照片的请求码:

private Uri mUri; // 文件的路径对象
private int CHOOSE_CODE = 3; // 选择照片的请求码

接着在选取按钮的点击方法中加入下面代码,表示打开系统相册选择照片:

// 创建一个内容获取动作的意图
Intent albumIntent = new Intent(Intent.ACTION_GET_CONTENT);
albumIntent.setType("image/*"); // 设置内容类型为图像
startActivityForResult(albumIntent, CHOOSE_CODE); // 打开系统相册,并等待图片选择结果

上面的跳转代码期望接收照片选择结果,于是重写当前活动的onActivityResult方法,调用返回意图的getData方法获得选中照片的路径对象,重写后的方法代码如下所示:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent)
{
super.onActivityResult(requestCode, resultCode, intent);
if (resultCode == RESULT_OK && requestCode == CHOOSE_CODE) { // 从相册选择一张
照片
if (intent.getData() != null) { // 数据非空,表示选中了某张照片
mUri = intent.getData(); // 获得选中照片的路径对象
iv_appendix.setImageURI(mUri); // 设置图像视图的路径对象
Log.d(TAG,
"uri.getPath="+mUri.getPath()+",uri.toString="+mUri.toString());
}
}
}

这下拿到了相册照片的路径对象,既能把它显示到图像视图,也能将它作为图片附件发送彩信了。由于普通应用无法自行发送彩信,必须打开系统的信息应用才行,于是编写页面跳转代码,往意图对象塞入详细的彩信数据,包括彩信发送的目标号码、标题、内容,以及Uri类型的图片附件。详细的跳转代码示例如下:

// 发送带图片的彩信
private void sendMms(String phone, String title, String message) {
Intent intent = new Intent(Intent.ACTION_SEND); // 创建一个发送动作的意图
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 另外开启新页面
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 需要读权限
intent.putExtra("address", phone); // 彩信发送的目标号码
intent.putExtra("subject", title); // 彩信的标题
intent.putExtra("sms_body", message); // 彩信的内容
intent.putExtra(Intent.EXTRA_STREAM, mUri); // mUri为彩信的图片附件
intent.setType("image/*"); // 彩信的附件为图片
// 部分手机无法直接跳到彩信发送页面,故而需要用户手动选择彩信应用
//intent.setClassName("com.android.mms","com.android.mms.ui.ComposeMessageActiv
ity");
startActivity(intent); // 因为未指定要打开哪个页面,所以系统会在底部弹出选择窗口
ToastUtil.show(this, "请在弹窗中选择短信或者信息应用");
}

运行测试App,刚打开的活动页面如图7-9所示,在各行编辑框中依次填写彩信的目标号码、标题、内容,再到系统相册选取照片,填好的界面效果如图7-10所示。
在这里插入图片描述
在这里插入图片描述
之后点击发送按钮,屏幕下方弹出如图7-11所示的应用选择窗口。

先点击信息图标,表示希望跳到信息应用,再点击“仅此一次”按钮,此时打开信息应用界面如图7-12所示。可见信息发送界面已经自动填充收件人号码、信息标题和内容,以及图片附件,只待用户轻点右下角的飞鸽传书图标,就能将彩信发出去了。
在这里插入图片描述
在这里插入图片描述

8.2借助FileProvider发送彩信

通过系统相册固然可以获得照片的路径对象,却无法知晓更多的详细信息,例如照片名称、文件大小、文件路径等信息,也就无法进行个性化的定制开发。为了把更多的文件信息开放出来,Android设计了专门的媒体共享库,允许开发者通过内容组件从中获取更详细的媒体信息。

图片所在的相册媒体库路径为MediaStore.Images.Media.EXTERNAL_CONTENT_URI,通过内容解析器即可从媒体库依次遍历得到图片列表详情。为便于代码管理,首先要声明如下的对象变量:

private List<ImageInfo> mImageList = new ArrayList<ImageInfo>(); // 图片列表
private Uri mImageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; // 相册的
Uri
private String[] mImageColumn = new String[]{ // 媒体库的字段名称数组
MediaStore.Images.Media._ID, // 编号
MediaStore.Images.Media.TITLE, // 标题
MediaStore.Images.Media.SIZE, // 文件大小
MediaStore.Images.Media.DATA}; // 文件路径

然后使用内容解析器查询媒体库的图片信息,简单起见只挑选文件大小最小的前6张图片,图片列表加载代码示例如下:

// 加载图片列表
private void loadImageList() {
mImageList.clear(); // 清空图片列表
// 查询相册媒体库,并返回结果集的游标。“_size asc”表示按照文件大小升序排列
Cursor cursor = getContentResolver().query(mImageUri, mImageColumn, null,
null, "_size asc");
if (cursor != null) {
// 下面遍历结果集,并逐个添加到图片列表。简单起见只挑选前六张图片
for (int i=0; i<6 && cursor.moveToNext(); i++) {
ImageInfo image = new ImageInfo(); // 创建一个图片信息对象
image.setId(cursor.getLong(0)); // 设置图片编号
image.setName(cursor.getString(1)); // 设置图片名称
image.setSize(cursor.getLong(2)); // 设置图片的文件大小
image.setPath(cursor.getString(3)); // 设置图片的文件路径
Log.d(TAG, image.getName() + " " + image.getSize() + " " +
image.getPath());
if (!FileUtil.checkFileUri(this, image.getPath())) { // 检查该路径是否
合法
i--;
continue; // 路径非法则再来一次
}
mImageList.add(image); // 添加至图片列表
}
cursor.close(); // 关闭数据库游标
}
}

注意到以上代码获得了字符串格式的文件路径,而彩信发送应用却要求Uri类型的路径对象,原本可以通过代码“Uri.parse(path)”将字符串转换为Uri对象,但是从Android 7.0开始,系统不允许其他应用直接访问老格式的路径,必须使用文件提供器FileProvider才能获取合法的Uri路径,相当于A应用申明了共享某个文件,然后B应用方可访问该文件。为此需要重头配置FileProvider,详细的配置步骤说明如下。

首先在res目录新建xml文件夹,并在该文件夹中创建file_paths.xml,再往XML文件填入以下内容,表示定义几个外部文件目录:

<paths>
<external-path path="Android/data/com.example.chapter07/" name="files_root"
/>
<external-path path="." name="external_storage_root" />
</paths>

接着打开AndroidManifest.xml,在application节点内部添加下面的provider标签,表示声明当前应用的内容提供器组件,添加后的标签配置示例如下:

<!-- 兼容Android7.0,把访问文件的Uri方式改为FileProvider -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/file_provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

上面的provider有两处地方允许修改,一处是authorities属性,它规定了授权字符串,这是每个提供器的唯一标识;另一处是元数据的resource属性,它指明了文件提供器的路径资源,也就是刚才定义的file_paths.xml。

回到活动页面的源码,在发送彩信之前添加下述代码,目的是根据字符串路径构建Uri对象,注意针对Android 7.0以上的兼容处理。

Uri uri = Uri.parse(path); // 根据指定路径创建一个Uri对象
// 兼容Android7.0,把访问文件的Uri方式改为FileProvider
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 通过FileProvider获得文件的Uri访问方式
uri = FileProvider.getUriForFile(this,
"com.example.chapter07.fileProvider", new
File(path));
}

由以上代码可见,Android 7.0开始调用FileProvider的getUriForFile方法获得Uri对象,该方法的第二个参数为文件提供器的授权字符串,第三个参数为File类型的文件对象。
运行测试App,页面会自动加载媒体库的前6张图片,另外手工输入对方号码、彩信标题、彩信内容等信息,填好的发送界面如图7-13所示。
在这里插入图片描述
点击页面下方的某张图片,表示选中该图片作为彩信附件,此时界面下方弹出如图7-14所示的应用选择窗口。选中信息图标再点击“仅此一次”按钮,即可跳到如图7-15所示的系统信息发送页面了。

在这里插入图片描述

8.3借助FileProvider安装应用

除了发送彩信需要文件提供器,安装应用也需要FileProvider。不单单彩信的附件图片能到媒体库中查询,应用的APK安装包也可在媒体库找到。查找安装包依然借助于内容解析器,具体的实现过程和查询图片类似,比如事先声明如下的对象变量:

private List<ApkInfo> mApkList = new ArrayList<ApkInfo>(); // 安装包列表
private Uri mFilesUri = MediaStore.Files.getContentUri("external"); // 存储卡的Uri
private String[] mFilesColumn = new String[]{ // 媒体库的字段名称数组
MediaStore.Files.FileColumns._ID, // 编号
MediaStore.Files.FileColumns.TITLE, // 标题
MediaStore.Files.FileColumns.SIZE, // 文件大小
MediaStore.Files.FileColumns.DATA, // 文件路径
MediaStore.Files.FileColumns.MIME_TYPE}; // 媒体类型

再通过内容解析器到媒体库查找安装包列表,具体的加载代码示例如下:

// 加载安装包列表
private void loadApkList() {
mApkList.clear(); // 清空安装包列表
// 查找存储卡上所有的apk文件,其中mime_type指定了APK的文件类型,或者判断文件路径是否.apk结尾
Cursor cursor = getContentResolver().query(mFilesUri, mFilesColumn,
"mime_type='application/vnd.android.package-archive' or _data like '%.apk'",
null, null);
if (cursor != null) {
// 下面遍历结果集,并逐个添加到安装包列表。简单起见只挑选前十个文件
for (int i=0; i<10 && cursor.moveToNext(); i++) {
ApkInfo apk = new ApkInfo(); // 创建一个安装包信息对象
apk.setId(cursor.getLong(0)); // 设置安装包编号
apk.setName(cursor.getString(1)); // 设置安装包名称
apk.setSize(cursor.getLong(2)); // 设置安装包的文件大小
apk.setPath(cursor.getString(3)); // 设置安装包的文件路径
Log.d(TAG, apk.getName() + ", " + apk.getSize() + ", " +
apk.getPath()+", "+cursor.getString(4));
if (!FileUtil.checkFileUri(this, apk.getPath())) { // 检查该路径是否合法
i--;
continue; // 路径非法则再来一次
}
mApkList.add(apk); // 添加至安装包列表
}
cursor.close(); // 关闭数据库游标
}
}

找到安装包之后,通常还要获取它的包名、版本名称、版本号等信息,此时可调用应用包管理器的getPackageArchiveInfo方法,从安装包文件中提取PackageInfo包信息。包信息对象的packageName属性值为应用包名,versionName属性值为版本名称,versionCode属性值为版本号。下面是利用弹窗展示包信息的代码例子:

// 显示安装apk的提示对话框
private void showAlert(final ApkInfo apkInfo) {
PackageManager pm = getPackageManager(); // 获取应用包管理器
// 获取apk文件的包信息
PackageInfo pi = pm.getPackageArchiveInfo(apkInfo.getPath(),
PackageManager.GET_ACTIVITIES);
if (pi != null) { // 能找到包信息
Log.d(TAG, "packageName="+pi.packageName+",
versionName="+pi.versionName+", versionCode="+pi.versionCode);
String desc = String.format("应用包名:%s\n版本名称:%s\n版本编码:%s\n文件路
径:%s",
pi.packageName, pi.versionName,
pi.versionCode, apkInfo.getPath());
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("是否安装该应用?"); // 设置提醒对话框的标题
builder.setMessage(desc); // 设置提醒对话框的消息内容
builder.setPositiveButton("是", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
installApk(apkInfo.getPath()); // 安装指定路径的APK
}
});
builder.setNegativeButton("否", null);
builder.create().show(); // 显示提醒对话框
} else { // 未找到包信息
ToastUtil.show(this, "该安装包已经损坏,请选择其他安装包");
}
}

有了安装包的文件路径之后,就能打开系统自带的安装程序执行安装操作了,此时一样要把安装包的Uri对象传过去。应用安装的详细调用代码如下所示:

// 安装指定路径的APK
private void installApk(String path) {
Log.d(TAG, "path="+path);
Uri uri = Uri.parse(path); // 根据指定路径创建一个Uri对象
// 兼容Android7.0,把访问文件的Uri方式改为FileProvider
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 通过FileProvider获得安装包文件的Uri访问方式
uri = FileProvider.getUriForFile(this,
getPackageName()+".fileProvider", new
File(path));
}
Intent intent = new Intent(Intent.ACTION_VIEW); // 创建一个浏览动作的意图
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // 另外开启新页面
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 需要读权限
// 设置Uri的数据类型为APK文件
intent.setDataAndType(uri, "application/vnd.android.package-archive");
startActivity(intent); // 启动系统自带的应用安装程序
}

注意,从Android 8.0开始,安装应用需要申请权限REQUEST_INSTALL_PACKAGES,于是打开AndroidManifest.xml,补充下面的权限申请配置:

<!-- 安装应用请求,Android8.0需要 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

这下大功告成,编译运行App,打开测试页面自动加载安装包列表的界面如图7-16所示。点击某项安装包,弹出如图7-17所示的确认对话框。
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1974811.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kubernetes中的CRI、CNI与CSI:深入理解云原生存储、网络与容器运行时

引言 随着云原生技术的飞速发展&#xff0c;Kubernetes&#xff08;简称K8s&#xff09;作为云原生应用的核心调度平台&#xff0c;其重要性日益凸显。K8s通过开放一系列接口&#xff0c;实现了高度的可扩展性和灵活性&#xff0c;其中CRI&#xff08;Container Runtime Inter…

使用归一化连接计数的胸部CT成像:预测CanCOLD研究中的肺气肿进展| 文献速递-AI辅助的放射影像疾病诊断

Title 题目 CT Chest Imaging Using Normalized Join-Count: Predicting Emphysema Progression in the CanCOLD Study 使用归一化连接计数的胸部CT成像&#xff1a;预测CanCOLD研究中的肺气肿进展 Background 背景 Pre-existing emphysema is recognized as an indicator…

【C++】------继承(一)

目录 前言 一、概念与定义 Ⅰ、是什么&#xff1f; Ⅱ、定义 1.定义格式&#xff1a; 2.继承方式和访问限定符 3.基类&#xff08;父类&#xff09;成员访问方式的变化 二、父类与子类的赋值转化 基本认识 原理 三、 继承中的作用域 四、子类(派生类)的默认成员函…

Spring中是如何实现IoC和DI的?

前言&#xff1a;在前一篇文章中对于IoC的核心思想进行了讲解&#xff0c;而本篇文章则从Spring的角度入手&#xff0c;体会Spring对于IoC是如何实现的。 如果对IoC还有不太了解的可以阅读上一篇文章&#xff0c;相信一定会带来全新的收获&#xff1a;什么是IoC&#xff08;控制…

5.5软件工程-系统测试

系统测试 意义和目的原则测试过程测试策略测试方法练习题 测试用例设计黑盒测试等价类划分边界值分析错误推测因果图 白盒测试逻辑覆盖循环覆盖基本路径测试法 练习题 调试软件度量练习题 考点少&#xff0c;知识点多 意义和目的 系统测试的意义&#xff1a;系统测试是为了发现…

浅谈Redis集群架构与主从架构

目录 1. Redis集群1.1 集群概念1.2 集群分片1.3 重新分片 2. 集群的主从模型2.1 主从模型2.2 主节点选举 1. Redis集群 1.1 集群概念 面试官&#xff1a;我看你简历写了Redis集群&#xff0c;你说一说&#xff1f; Redis主从架构和Redis集群架构是两种不同的概念&#xff0c;大…

【Spring成神之路】从源码角度深度刨析Spring循环依赖

文章目录 一、引言二、循环依赖出现的场景2.1 有参构造导致的循环依赖问题2.2 属性注入出现的依赖问题2.3 Spring IOC创建Bean的流程2.4 有参构造为何失败2.5 属性注入为何能成功2.6 AOP导致的循环依赖 三、Spring循环依赖源码刨析四、Spring循环依赖案例刨析 一、引言 循环依…

【MATLAB源码】数学建模基础教程---初步认识数学建模

系列文章目录在最后面&#xff0c;各位同仁感兴趣可以看看&#xff01; 什么是数学建模 含义1.区分数学模型和数学建模2. 建立数学模型的注意事项3.数学建模流程图解4.数学建模模型分类5.论文常用套路6.最后&#xff1a;总结系列文章目录 含义 所谓数学建模&#xff0c;简言…

Python 中实现聊天客户端库

在 Python 中实现一个简单的聊天客户端库可以通过使用 socket 模块来处理网络通信。我们可以构建一个基于 TCP 的简单聊天系统&#xff0c;其中包括一个服务器和一个客户端。 1、问题背景 假设您正在尝试编写一个 Python 库&#xff0c;用于实现某个聊天协议的客户端。在连接…

c++入门基础(下篇)————引用、inline、nullptr

引用 引用的概念和定义 引⽤不是新定义⼀个变量&#xff0c;⽽是给已存在变量取了⼀个别名&#xff0c;编译器不会为引⽤变量开辟内存空间&#xff0c; 它和它引⽤的变量共⽤同⼀块内存空间。 类型& 引用别名 引用对象; 就像孙悟空也叫齐天大圣 猪八戒也叫天蓬元帅。…

正点原子imx6ull-mini-Linux驱动之Linux 自带的 LED 灯驱动实验(16)

前面我们都是自己编写 LED 灯驱动&#xff0c;其实像 LED 灯这样非常基础的设备驱动&#xff0c;Linux 内 核已经集成了。Linux 内核的 LED 灯驱动采用 platform 框架&#xff0c;因此我们只需要按照要求在设备 树文件中添加相应的 LED 节点即可&#xff0c;本章我们就来学习如…

Level3答案

突然发现&#xff0c;忘记公布了Level3答案&#xff1a; 1、 (1)heker.h HeiKe.h (2)Make_Text() (3)3 (4)heker.h 2、 (1)ArtText.h Maker_World.h (Maker_Game头文件组) (2)5.0 附加题、 我把标题截了张图&#xff01; 这是我们 Cookie Maker工作室 新出来的 “无标题技术”…

JavaScript基础——JavaScript数据及数据类型

JavaScript中数据的分类 数据是指设备、浏览器可以识别的内容。在JavaScript中&#xff0c;数据可分为基本数据类型&#xff08;值数据类型&#xff09;和引用数据类型。 console.log()函数 浏览器中按下F12或者右击检查&#xff0c;可以打开控制台。 在JavaScript中&#xff0…

微服务通过X-Forwarded-For获取客户端最原始的IP地址

文章目录 引言I 通过转发IP列表获取用户的IP地址II 存储真实IP字段到MDC中2.1 自己存储真实IP字段,方便获取。2.2 feign 传递MDC数据(将MDC中数据传入header)III 处理真实IP(应用)3.1 从MDC获取存储到日志系统中3.2 logback获取MDC数据(IP、追踪码)3.3 打印接口的请求IP引…

教你用python代码写一个中国象棋游戏

编写一个完整的中国象棋游戏是一个复杂的项目&#xff0c;因为它涉及到图形用户界面(GUI)的设计、游戏规则的实现、AI对手的开发等多个方面。不过&#xff0c;我可以提供一个简化的框架和一些基本思路&#xff0c;帮助你开始这个项目。 由于这里不能完整地实现一个图形化的象棋…

三十六、MyBatis-Plus(2)

&#x1f33b;&#x1f33b; 目录 一、CRUD 扩展&#xff08;1)1.1 Insert1.2 主键生成策略1.2.1 源码解释1.2.2 Twitter的snowflake算法 (雪花算法)1.2.3 主键自增&#xff1a;AUTO 我们需要配置主键自增1.2.4 手动输入&#xff1a;INPUT 就需要自己写 id 1.3 Update1.4 自动填…

2024杭电多校第五场

第一题&#xff1a;开关灯 直接暴力找规律。 发现如果n2&#xff08;mod3&#xff09;那么就是2的n-1次方。否则直接是2的n次方。 暴力代码 #include<bits/stdc.h> using namespace std; #define int long longsigned main() {int temp[100];temp[0] 1;for (int i …

SOMEIP_ETS_001:数组长度超过消息长度允许的范围

测试目的&#xff1a; 验证DUT&#xff08;Device Under Test&#xff0c;被测设备&#xff09;在接收到数组长度超过SOME/IP协议允许的最大长度时&#xff0c;是否能够返回错误消息。 描述 本测试用例旨在检查DUT在接收到一个SOME/IP消息时&#xff0c;如果该消息中的数组长…

Java学习:今日成果,明日挑战

阅读指南&#xff1a;[题目] - 精选摘要 题目1.面向对象编程意味着2.以下哪项不是 Java 关键字&#xff1f;3.基础数据类型在堆栈上分配&#xff1f;4.以下代码将导致&#xff1a;5.以下输出是什么 &#xff1f;6.如果我们声明&#xff1a;7.Java 使用按值调用。 以下方法调用传…

S7-1200PLC 和8块欧姆龙温控表MODBUS通信(完整SCL代码)

1、如何提升MODBUS-RTU通信数据的刷新速度 提升MODBUS-RTU通信数据刷新速度的常用方法_modbus rtu通讯慢-CSDN博客文章浏览阅读1.2k次。SMART PLC的MODBUS-RTU通信请参考下面文章链接:【精选】PLC MODBUS通信优化、提高通信效率避免权限冲突(程序+算法描述)-CSDN博客MODBU…