乐趣区

关于java:探究ContentProvider

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

<!– more –>

自定义 ContentProvider

step1、自定义类继承于 ContentProvider,实现要求的办法
step2、在配置文件中通过 provider 标签配置,通过 android:name 属性指定待配置的类,通过 android:authorities 属性受权,指定以后内容提供者的 uri 标识,必须惟一。

上面来展现一个 B 利用来操作 A 利用中的数据的例子:

首先在 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;
        }
    }
}

能够看到,其实应用 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);

大家在今后的开发中可能会用到更多的匹配模式,接下来咱们学习 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;

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);    
}

果然通过这样的 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();}

增加通讯录

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));
    }
}

读取短信

短信类型 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"/>

ContentProvider 的长处

ContentProvider 的底层实现是 Binder,更多对于 Binder 的内容能够参考官网文档《Binder》。ContentProvider 为利用间的数据交互提供了一个平安的环境:容许把本人的利用数据依据需要凋谢给其余利用进行 CRUD,而不必放心因为间接凋谢数据库权限而带来的平安问题。而其余对外共享数据的形式,数据拜访形式会因数据存储的形式而不同而发生变化,底层存储形式变更会影响下层,使拜访数据变得更加简单。而采纳 ContentProvider 形式,其解耦了底层数据的存储形式,使得无论底层数据存储采纳何种形式,外界对数据的拜访形式都是对立的,这使得拜访简略且高效。

原文地址:《探索 ContentProvider》

退出移动版