7

探究ContentProvider

 3 years ago
source link: https://zouchanglin.cn/2020/12/09/24594.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

为什么微信、QQ、淘宝等App都能访问联系人(通讯录)呢?是因为Android存在一种应用之间的数据共享机制,即ContentProvider,ContentProvider作为Android四大组件之一,为存储和获取数据提供统一的接口,可以在不同的应用程序之间共享数据。对于ContentProvier而言,无论数据的来源是什么,它都认为是种表(同时也支持文件数据,只是表格形式用得比较多),然后把数据组织成表格返回给使用者。

自定义ContentProvider

step1、自定义类继承于ContentProvider,实现要求的方法

step2、在配置文件中通过provider标签配置,通过android:name属性指定待配置的类,通过android:authorities属性授权,指定当前内容提供者的uri标识,必须唯一。

下面来展示一个B应用来操作A应用中的数据的例子:

20201209154026.png-zouchanglin.cn

首先在ContentProviderDemo这个工程里写一个名为MyContentProvider的ContentProvider:

public class MyContentProvider extends ContentProvider {
    private static final String TAG = "MyContentProvider";
    private SQLiteDatabase sqLiteDatabase;

    public MyContentProvider() {
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        Log.i(TAG, "delete: ");
        return sqLiteDatabase.delete("stu_info", selection, selectionArgs);
    }

    @Override
    public String getType(Uri uri) {
        // TODO: Implement this to handle requests for the MIME type of the data
        // at the given URI.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Log.i(TAG, "insert: ");
        // 参数解释:操作表的名称、可以为空的列、参数
        sqLiteDatabase.insert("stu_info", null, values);
        return uri;
    }

    // 在ContentProvider创建时调用
    @Override
    public boolean onCreate() {
        SQLiteOpenHelper helper = new SQLiteOpenHelper(getContext(), "stu.db", null, 1) {
            @Override
            public void onCreate(SQLiteDatabase db) {
                db.execSQL("create table stu_info (id integer primary key autoincrement," +
                        " name varchar(30) not null, age integer," +
                        " gender varchar(2) not null)");
                Log.i(TAG, "onCreate: 数据库创建成功");
            }

            @Override
            public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

            }
        };
        sqLiteDatabase = helper.getWritableDatabase();
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        Log.i(TAG, "query: ");
        return sqLiteDatabase.query("stu_info", projection, selection, selectionArgs, sortOrder, null, null);
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        Log.i(TAG, "update: ");
        return sqLiteDatabase.update("stu_info", values, selection, selectionArgs);
    }
}

对于四大组件之一的ContentProvider同样需要在AndroidManifest.xml中声明:

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

    <application
        android:allowBackup="true"
        ....
        android:theme="@style/AppTheme">
        <provider
            android:name=".MyContentProvider"
            android:authorities="cn.tim.myprovider"
            android:enabled="true"
            android:exported="true" />
		...
    </application>
</manifest>

必须通过android:name属性指定待配置的类,通过android:authorities属性授权,指定当前内容提供者的uri标识,必须唯一,因为对于使用ContentProvier的App来说,这是唯一可以找到该ContentProvier的信息,就像坐标一样,是唯一可以确定你的位置的信息。

可以看到在这个类里面主要包含了CRUD等方法,还有getType()方法和onCreate()方法。所以为什么说对于ContentProvier而言,无论数据的来源是什么,它都认为是种表,然后把数据组织成表格。因为这恰好对应了表中数据的CRUD。至于getType()方法是做什么现在可以不管,整个MyContentProvider不过是在初始化的时候创建了数据库,拿到了SQLiteDatabase对象,然后MyContentProvider其中的CRUD方法实现成了数据库的操作方法而已。如果对数据库不太熟悉,可以参考之前的文章《SQLite原理与运用》,里面有具体介绍使用方法。

值得注意的是,虽然我们在MyContentProvider的CRUD中使用了SQLite数据库,但是其实这和ContentProvider本身并没有关系,数据的增删改查我们完全也可以用HashMap这种数据结构存在内存中,或者存成文件的形式,一行文本就代表一个数据对象,这里为了方便演示所以直接采用了SQLite。

ContentProviderDemo这个工程就结束了,因为作为内容提供者,它无需提供操作界面。下面看看使用者,也就是图中的OtherApplication。当然在这个示例中,这个工程名称为ContentAcquireDemo,界面和《SQLite原理与运用》中的界面一模一样,只不过是在CRUD的时候不再是操作本地SQLite,而是操作ContentProviderDemo中的MyContentProvider:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    ContentResolver contentResolver;
    private EditText etId;
    private EditText etName;
    private EditText etAge;
    private String sex = "男";
    private ListView lvData;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        etId = findViewById(R.id.et_id);
        etName = findViewById(R.id.et_name);
        etAge = findViewById(R.id.et_age);

        // 单选框组件
        RadioGroup rgSex = findViewById(R.id.rg_sex);
        lvData = findViewById(R.id.lv_data);

        // 获取ContentResolver对象
        contentResolver = getContentResolver();

        // 设置单选框的监听
        rgSex.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
            @Override
            public void onCheckedChanged(RadioGroup group, int checkedId) {
                switch (checkedId){
                    case R.id.rb_female:
                        sex = "女";
                        break;
                    case R.id.rb_male:
                        sex = "男";
                        break;
                }
            }
        });
        flushStuData();
    }

    private void flushStuData() {
        Uri uri = Uri.parse("content://cn.tim.myprovider");
        List<StudentInfo> stuList = new ArrayList<>();
        // 参数解释:表名、要查询的字段、列条件、列条件参数、GroupBy、having、orderBy
        Cursor cursor = contentResolver.query(uri, null, null, null, null);
        if(cursor !=null && cursor.moveToFirst()){
            do{
                int id = cursor.getInt(0);
                String name = cursor.getString(1);
                int age = cursor.getInt(2);
                String sex = cursor.getString(3);
                stuList.add(new StudentInfo(id, name, age, sex));
            } while (cursor.moveToNext());
            cursor.close();
        }
        lvData.setAdapter(new StuInfoAdapter(this, stuList));
    }


    public void operatorData(View view) {
        Uri uri = Uri.parse("content://cn.tim.myprovider");
        int viewId = view.getId();
        switch (viewId) {
            case R.id.btn_add:
                ContentValues values = new ContentValues();
                values.put("name", etName.getText().toString());
                values.put("age", Integer.parseInt(etAge.getText().toString()));
                values.put("gender", sex);
                contentResolver.insert(uri, values);
                // 刷新数据展示
                flushStuData();
                Toast.makeText(MainActivity.this, "添加成功", Toast.LENGTH_SHORT).show();
                break;
            case R.id.btn_update:
                String idStr = etId.getText().toString();
                ContentValues updateValues = new ContentValues();
                // Key - value
                updateValues.put("name", etName.getText().toString());
                updateValues.put("age", Integer.parseInt(etAge.getText().toString()));
                updateValues.put("gender", sex);
                contentResolver.update(uri, updateValues, "id=?", new String[]{idStr});
                Toast.makeText(MainActivity.this, "更新成功", Toast.LENGTH_SHORT).show();
                flushStuData();
                break;
            case R.id.btn_delete:
                String deleteIdStr = etId.getText().toString();
                contentResolver.delete(uri, "id=?", new String[]{deleteIdStr});
                // 刷新数据展示
                flushStuData();
                Toast.makeText(MainActivity.this, "删除成功", Toast.LENGTH_SHORT).show();
                break;
        }
    }
}

20201209161256.png-zouchanglin.cn

可以看到,其实使用content://cn.tim.myprovider这个ContentProvider同样达到了CRUD的效果,需要注意的就是别把URI写错了就行,所以下面来看看URI的解析:

Uri匹配之UriMatcher

UriMatcher:在ContentProvider创建时,制定好匹配规则,当调用了ContentProvider中的操作方法时,利用匹配类去匹配传的uri,根据不同的uri给出不同的处理。

现在在MyContentProvider的onCrate()方法中定义一个UriMatcher匹配器,并且给出匹配规则如下:

public class MyContentProvider extends ContentProvider {
   	...
	private UriMatcher matcher;
    
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        Log.i(TAG, "delete: ");
        int matchCode = matcher.match(uri);
        switch (matchCode){
            case 1001:
                Log.i(TAG, "delete: 匹配到路径是/hello");
                break;
            default:
                Log.i(TAG, "delete: 执行删除数据库内容操作");
                return sqLiteDatabase.delete("stu_info", selection, selectionArgs);
        }
        return 0;
    }
	...
    // 在ContentProvider创建时调用
    @Override
    public boolean onCreate() {
        ...
        sqLiteDatabase = helper.getWritableDatabase();

        // 参数代表无法匹配
        // content://cn.tim.myprovider/hello
        matcher = new UriMatcher(UriMatcher.NO_MATCH);
        // Authority 、路径、匹配码
        matcher.addURI("cn.tim.myprovider", "hello", 1001);
        return true;
    }
    ...
}

这样在另一个App中使用MyContentProvider的delete()方法的时候就会进行URI匹配判断:

contentResolver.delete(Uri.parse("content://cn.tim.myprovider/hello"), null, null);

20201209191259.png-zouchanglin.cn

大家在今后的开发中可能会用到更多的匹配模式,接下来我们学习UriMatcher更多匹配:

UriMatcher还可以使用匹配通配符来匹配任意不确定的值:

matcher = new UriMatcher(UriMatcher.NO_MATCH);
// Authority 、路径、匹配码
matcher.addURI("cn.tim.myprovider", "hello", 1001);

// 匹配 cn.tim.myprovider/hello/任意数字
matcher.addURI("cn.tim.myprovider", "hello/#", 1002);

// 匹配 cn.tim.myprovider/world/任意字符串
matcher.addURI("cn.tim.myprovider", "world/*", 1003);

return true;

20201209192151.png-zouchanglin.cn

Uri与Uri自带的解析方法

使用Uri自带的解析方法

现在ContentAcquireDemo假设添加方法是这样调用的,即把参数写在Uri里面,这样在ContentProviderDemo工程的MyContentProvider中又是如何解析的呢?

Uri insertUri = Uri.parse("content://cn.tim.myprovider/whatever?name=Tim&age=22&gender=男");
Uri uri = contentResolver.insert(insertUri, new ContentValues());
long newId = ContentUris.parseId(uri);
Toast.makeText(this, "添加成功: Id" + newId, Toast.LENGTH_SHORT).show();

MyContentProvider.java的关键代码:

@Override
public Uri insert(Uri uri, ContentValues values) {
        long id = 0;
    	// 为了保持原来的方式不做变更,所以这里需要判断一下
        if(values.size() > 0){
            id = sqLiteDatabase.insert("stu_info", null, values);
        }else {
            String authority = uri.getAuthority();
            String path = uri.getPath();
            String query = uri.getQuery();
            String name = uri.getQueryParameter("name");
            String age = uri.getQueryParameter("age");
            String gender = uri.getQueryParameter("gender");
            Log.i(TAG, "insert:->主机名:" + authority
                    + ",路径:" + path + ",查询数据:" + query + ",姓名:" + name
            + ",age:" + age + ",gender:" + gender);
            values.put("name", name);
            values.put("age", age);
            values.put("gender", gender);
            id = sqLiteDatabase.insert("stu_info", null, values);
        }
        return ContentUris.withAppendedId(uri, id);    
}

20201209194949.png-zouchanglin.cn

果然通过这样的Uri自带的解析方式来传递参数也是OK的。

关于Uri必须知道的

这样的解析方式涉及到Uri的组成和结构问题,首先来说一说URI和Uri是什么关系吧,Uri是Android的API,扩展了JavaSE中URI的一些功能来特定的适用于Android开发,所以大家在开发时,只使用Android 提供的Uri即可。

Uri统一资源标识符(Uniform Resource Identifier),有时我们又看到URL这样的东西,他们之间的又是什么关系呢?统一资源标志符URI就是在某一规则下能把一个资源独一无二地标识出来,比如想要标识一个我国公民,只要用身份证号就可以作为唯一标识,但是使用其他方式也可以用来标识唯一个人,比如:个人定位协议://中华人名共和国/陕西省/西安市/临潼区/斜口街道/西安工程大学/8#宿舍/A120/邹长林, 这个字符串同样标识出了唯一的一个人,起到了URI的作用,所以URL是URI的子集。URL是以描述人的位置来唯一确定一个人的。

所以统一资源标志符URI就是在某一规则下能把一个资源独一无二地标识出来,URL就是某主机上的某路径上的文件来唯一确定一个资源,也就是定位的方式来实现的URI,即URL是URI的一种实现。

关于更多Uri结构和代码提取的资料可以参考 《Uri详解之——Uri结构与代码提取》

使用系统的ContentProvider

下面是通过读写系统通讯录和读取短信的几个小例子,作为ContentProvider使用练习:

读取通讯录

public void visitAddressBook(View view) {
    ContentResolver resolver = getContentResolver();
    //联系人姓名 + Id
    Uri uri = ContactsContract.Contacts.CONTENT_URI;
    //联系人电话
    Uri uriPhone = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
    Cursor cursor = resolver.query(uri, null, null, null, null);
    while(cursor!= null && cursor.moveToNext()){
        String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
        String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));
        Log.i(TAG, "visitAddressBook: name = " + name + ", id = " + contactId);
        String selection = ContactsContract.CommonDataKinds.Phone.CONTACT_ID + "=" +contactId;
        Cursor phoneCursor = resolver.query(uriPhone,null, selection, null, null);
        while (phoneCursor != null && phoneCursor.moveToNext()){
            String phone = phoneCursor.getString(phoneCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
            Log.i(TAG, "visitAddressBook: name = " + name + ", phone = " + phone);
        }
        if(phoneCursor != null) phoneCursor.close();
    }
    if(cursor != null) cursor.close();
}

20201209222954.png-zouchanglin.cn

添加通讯录

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    // 申请权限请求码
    private static final int REQUEST_READ_SMS = 1001;

    // 检查权限,这种写法主要是针对比较新的Android6.0及以后的版本
    public static void verifyStoragePermissions(Activity activity) {
        int smsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_SMS);
        int contactsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS);
        int writeContactsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_CONTACTS);

        if (smsPermission != PackageManager.PERMISSION_GRANTED
                || contactsPermission != PackageManager.PERMISSION_GRANTED
                || writeContactsPermission != PackageManager.PERMISSION_GRANTED) {
            // 如果没有权限需要动态地去申请权限
            ActivityCompat.requestPermissions(
                    activity,
                    // 权限数组
                    new String[]{Manifest.permission.READ_SMS, Manifest.permission.READ_CONTACTS,  Manifest.permission.WRITE_CONTACTS},
                    // 权限请求码
                    REQUEST_READ_SMS
            );
        }
    }

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

    public void addAddressBook(View view) {
        //1、往一个ContentProvider中插入一条空数据,获取新生成的Id
        //2、利用刚刚生成的Id分别组合姓名和电话号码往另一个ContentProvider中插入数据
        ContentValues values = new ContentValues();
        ContentResolver resolver = getContentResolver();
        Uri uri = resolver.insert(ContactsContract.RawContacts.CONTENT_URI, values);
        if(uri == null) throw new RuntimeException("插入新联系人失败");
        values.clear();
        long id = ContentUris.parseId(uri);
        // 插入姓名
        values.put(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, id);
        values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, "Mike");
        values.put(ContactsContract.CommonDataKinds.StructuredName.MIMETYPE,
                ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
        uri = resolver.insert(ContactsContract.Data.CONTENT_URI, values);
        if(uri != null) Log.i(TAG, "addAddressBook: 插入姓名,id = " + ContentUris.parseId(uri));
        //插入电话信息
        values.clear();
        values.put(ContactsContract.CommonDataKinds.Phone.RAW_CONTACT_ID, id);
        values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, "15720918678"); //添加号码
        values.put(ContactsContract.CommonDataKinds.Phone.MIMETYPE,
                ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
        values.put(ContactsContract.CommonDataKinds.Phone.TYPE,
                ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE); //添加号码类型
        uri = resolver.insert(ContactsContract.Data.CONTENT_URI, values);
        if(uri != null) Log.i(TAG, "addAddressBook: 插入电话号码,id = " + ContentUris.parseId(uri));
    }
}

20201209224353.png-zouchanglin.cn

读取短信

短信类型 Uri 短信箱 content://sms 收件箱 content://sms/inbox 发件箱 content://sms/sent 草稿箱 content://sms/draft
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    // 申请权限请求码
    private static final int REQUEST_READ_SMS = 1001;

    // 检查权限,这种写法主要是针对比较新的Android6.0及以后的版本
    public static void verifyStoragePermissions(Activity activity) {
        int smsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_SMS);
        int contactsPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS);

        if (smsPermission != PackageManager.PERMISSION_GRANTED
                || contactsPermission != PackageManager.PERMISSION_GRANTED) {
            // 如果没有权限需要动态地去申请权限
            ActivityCompat.requestPermissions(
                    activity,
                    // 权限数组
                    new String[]{Manifest.permission.READ_SMS, Manifest.permission.READ_CONTACTS},
                    // 权限请求码
                    REQUEST_READ_SMS
            );
        }
    }

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

    public void visitMessage(View view) {
        ContentResolver resolver = getContentResolver();
        Uri uri = Uri.parse("content://sms");
        Cursor cursor = resolver.query(uri, null, null, null, null);
        while(cursor != null && cursor.moveToNext()){
            int addressIndex = cursor.getColumnIndex("address");
            int bodyIndex = cursor.getColumnIndex("body");
            String address = cursor.getString(addressIndex);
            String body = cursor.getString(bodyIndex);
            Log.i(TAG, "visitMessage:" + address + ":" + body);
        }
        if(cursor != null) cursor.close();
    }
}

在AndroidManifest.xml配置一下权限:

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

20201209215346.png-zouchanglin.cn

ContentProvider的优点

ContentProvider的底层实现是Binder,更多关于Binder的内容可以参考官方文档 《Binder》 。ContentProvider为应用间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给其他应用进行CRUD,而不用担心因为直接开放数据库权限而带来的安全问题。而其他对外共享数据的方式,数据访问方式会因数据存储的方式而不同而发生变化,底层存储方式变更会影响上层,使访问数据变得更加复杂。而采用ContentProvider方式,其解耦了底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的,这使得访问简单且高效。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK