美文网首页
ContentProvider

ContentProvider

作者: code希必地 | 来源:发表于2020-10-09 15:34 被阅读0次

1、ContentProvider的简介

ContentProvider是跨程序共享数据的标准方式,它可以选择某一部分数据对外共享,从而保证隐私数据不会有泄露的风险。ContentProvider的使用方式有两种:

  • 1、使用现有的ContentProvider去读取或操作其他程序的数据。
  • 2、创建新的ContentProvider为外部程序提供数据访问的接口。
    Android系统中自带的通讯录、短信、媒体库等都提供了外部访问的接口,这就使得第三方程序可以访问这些数据,下面看下ContentProvider的使用。

2、ContentResolver的使用

对于每一个程序来说,如果要想访问ContentProvider中共享的数据,就需要使用ContentResolver类,可以通过Context.getContentResolver()来获取ContentResolver的实例。ContentResolver类中提供了和SQLiteDatabase类中相似的方法:query用于数据查询、delete删除数据、update更新数据、insert插入数据。
SQLiteDatabase操作数据是需要指定表名,表示操作某张表下的数据,但是对于跨程序操作共享数据的ContentResolver来说,如果只指定表名是不够的还需要指定操作那个程序下的表,所以ContentResolver使用Uri来指定哪个程序下的哪张表,Uri由authority和path组成的,authority用于区分程序的,为了防止冲突一般使用应用包名指定。比如某个程序的包名是com.example.app,那么authority就可以命名为com.example.app.provider。path是为了区分表的,比如应用程序中数据库中有table1和table2两张表,那么path分别命名为/table1/table2。然后把authority和path组合后com.example.app.provider/table1com.example.app.provider/table2。不过目前还无法看出它们是URI,还需要在前面加上协议名。因此URI的标准格式如下:

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

可以看到上面的URI可以很清晰的指出我们要操作哪个程序下的哪张表,这样我们就可以使用ContentResolver对数据进行操作了。 由于ContentResolver的增删改查方法和SQLiteDatabase类似,这里我们就简单学习下。

  • 1、ContentResolver的查询
public final  Cursor query(Uri uri,
                          String[] projection,
                          String selection,
                          String[] selectionArgs,
                      String sortOrder) {}

参数说明:

Uri uri:指定查询的表
String[] projection:要查询那些列,如column1,column2,column3
String selection:指定where条件,如where column=?
String[] selectionArgs:为where占位符提供具体的值
String sortOrder:指定查询结果进行排序的方式

query()方法依然返回Cursor对象,Cursor的使用在SQLiteDatabase中已经学习过了。

  • 2、ContentResolver的插入、删除、修改
//插入
public final Uri insert(Uri url,
                ContentValues values) {}
//删除
public final int delete(Uri url, String where,
            String[] selectionArgs) {}
//修改
public final int update(Uri uri,
            ContentValues values, String where,
            String[] selectionArgs) {}

学习了最复杂的查询数据,插入、删除、修改方法比较简单。这里不再赘述。下面就具体学习下如果访问ContentProvider中的具体数据。

3、读取系统联系人

没有什么复杂的逻辑,直接上代码

class QueryContactActivity : AppCompatActivity() {
    private val contactList = ArrayList<String>()
    private lateinit var adapter: ArrayAdapter<String>

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_query_contact)
        adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, contactList)
        contact_list.adapter = adapter
        btn_query_contact.setOnClickListener {
            initData()
        }


    }

    private fun initData() {
        if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.READ_CONTACTS
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_CONTACTS), 1)

        } else {
            getContactData()
        }

    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1 -> {
                if (!grantResults.isEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    getContactData()
                } else {
                    Toast.makeText(this, "请开启读取联系人权限", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }


    private fun getContactData() {
        val contentResolver = contentResolver
        contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null)?.apply {
            while (moveToNext()) {
                val name = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
                val phonNume = getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
                contactList.add("$name\n$phonNume")
            }
            close()
            adapter.notifyDataSetChanged()
        }

    }
}

学习完了访问现有的ContentProvider中的共享数据,下面我们看下如何自定义ContentProvider,为其他程序提供访问数据的接口。

4、创建自己的ContentProvider

下面就学习如何定义ContentProvider,为其他程序提供访问数据的接口并保证私密数据的安全性。

4.1、ContentProvider的创建

创建一个类继承ContentProvider并实现其中的方法

class MyContentProvider : ContentProvider() {

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        return null
    }

    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor? {
        return null
    }

    override fun onCreate(): Boolean {
        return false
    }

    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
        return 0
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        return 0
    }

    override fun getType(uri: Uri): String? {
        return null
    }
}
  • 1、onCreate():初始化ContentProvider时调用,通常在这里完成对数据库的创建和升级等操作,返回true表示ContentProvider初始化成功,返回false表示初始化失败。
  • 2、query():从ContentProvider中查询数据。uri表示从哪张表中查询数据,projection表示查询那些列,selection和selectionArgs参数用于查询那些行的数据,sortOrder表示查询数据的排序方式,查询结果通过Cursor对象返回。
  • 3、insert():将要插入的数据通过ContentValues组装,uri表示向哪个表中插入数据,返回表示这条新纪录的URI。
  • 4、update():通过Uri指定需要更新的表,更新数据通过ContentValues组装,selection和selectionArgs约束指定更新那些行,返回更新影响的行数。
    -5、delete():通过Uri指定哪个表需要删除数据,selection和selectionArgs约束指定删除那些行的数据,返回删除影响的行数。
    -6、getType():根据传入的Uri返回支持的MIME类型。

4.2、UriMatcher类

可以看到几乎所有的方法都和Uri相关,Uri的标准格式我们上面说过:content://com.example.app.provider/table1除此之外我们还可以在Uri后加上id,content://com.example.app.provider/table1/1表示操作table1表中id为1的数据。内容Uri的格式主要就上面两种,我们可以使用通配符来匹配以上两种方式。

  • *表示匹配任意长度的任意字符所以能够匹配任意表的Uri就可以写成content://com.example.app.provider/*
  • #表示匹配任意长度的数字,所以能够匹配表table1中所有数据的内容Uri可以写成content://com.example.app.provider/table1/#

接着我们还可以使用UriMatcher这个类轻松的实现Uri的功能,该类提供了两个方法:

  • addURI(String authority, String path, int code)通过该方法可以组建Uri,code是自定义代码和id无关,用于使用Uri.match(Uri uri)进行Uri的匹配
  • match(Uri uri)返回匹配传入Uri对象所对应的code,和addURI()组建URI传入的code对应。
    修改MyProvider中的代码
class MyContentProvider : ContentProvider() {
    private val tag = javaClass.name
    private val authority = "com.example.abu.listview.runtimepermissiondemo.provider"

    private val table1Dir = 0
    private val table1Item = 1
    private val table2Dir = 2
    private val table2Item = 3

    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)

    init {
        uriMatcher.addURI(authority, "table1", table1Dir)
        uriMatcher.addURI(authority, "table1/#", table1Item)
        uriMatcher.addURI(authority, "table2", table2Dir)
        uriMatcher.addURI(authority, "table2/#", table2Item)
    }

    //...

    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor? {
        when (uriMatcher.match(uri)) {
            table1Dir -> Log.e(tag, "查询table1中所有数据")
            table1Item -> Log.e(tag, "查询table1中单条数据")
            table2Dir -> Log.e(tag, "查询table2中所有数据")
            table2Item -> Log.e(tag, "查询table2中单条数据")
        }

        return null
    }

    //.....
}

下面看下调用ContentResolver.query()时,ContentProvider中query()输出的log。

 query_data.setOnClickListener {
            contentResolver.query(Uri.parse("content://$authority/table1"),null,null,null,null)
            contentResolver.query(Uri.parse("content://$authority/table1/1"),null,null,null,null)
            contentResolver.query(Uri.parse("content://$authority/table1/#"),null,null,null,null)
        }
//输出结果
MyContentProvider: 查询table1中所有数据
MyContentProvider: 查询table1中单条数据
MyContentProvider: 查询table1中所有数据

对此做如下的分析:

  • 1、uriMatcher.addURI(authority, "table1", table1Dir):期望匹配table1表中所有数据,所以在传入URI为content://com.example.app.provider/table1/#content://com.example.app.provider/table1UriMatcher.match()返回自定义code即table1Dir
  • 2、uriMatcher.addURI(authority, "table1/#", table1Item):期望匹配table1表中某一行的数据,所以当传入的URI为content://com.example.app.provider/table1/1UriMatcher.match()返回自定义code即table1Item,而当传入的Uri是content://com.example.app.provider/table1/#时却不匹配,故而UriMatcher.match()返回-1。

4.3、getType()

除此之外,还有一个比较陌生的就是getType()方法,返回Uri支持的MIME类型,一个Uri对应的MIME字符串有3部分组成:

  • 1、必须以vnd开头。
  • 2、如果内容URI以路径结尾,则后接android.cursor.dir/;如果内容URI以id结尾,则后接android.cursor.item/
  • 3、最后接上vnd.<authority>.<path>。
    所以,对于content://com.example.app.provider/table1/1这个Uri对应的MIMIE为:
vnd.android.cursor.item/vnd.com.example.app.provider.table1

对于content://com.example.app.provider/table1这个Uri对应的MIMIE为:

vnd.android.cursor.dir/vnd.com.example.app.provider.table1

现在我们可以修改一下MyContentProvider的getType()方法

override fun getType(uri: Uri): String? {
        when (uriMatcher.match(uri)) {
            table1Dir -> return "vnd.android.cursor.dir/vnd.$packageName.table1"
            table1Item -> return "vnd.android.cursor.item/vnd.$packageName.table1"
            table2Dir -> return "vnd.android.cursor.dir/vnd.$packageName.table2"
            table2Item -> "vnd.android.cursor.item/vnd.$packageName.table2"
        }
        return null
    }

4.4、ContentProvider的安全

到这里一个完整的ContentProvider就创建完成了,那么如何解决隐私数据不会泄露呢?其实很简单,上面我们在UriMatcher中添加了一些Uri,只有和我们添加的Uri匹配的才能进行数据的增删改查,只要我们不添加隐私数据的Uri即可解决隐私数据的安全问题。

5、实现跨程序数据共享

创建DatabaseProvider类继承ContentProvider,并在AndroidManifest中进行配置,指定authorities为com.example.abu.listview.databaseproviderdemo.provider,enabled为true表示可用,exported指定为true表示可以供外部程序使用。

<provider android:authorities="com.example.abu.listview.databaseproviderdemo.provider"
                  android:exported="true"
                  android:enabled="true"
                  android:name=".DatabaseProvider"/>

下面看下DatabaseProvider的具体实现

class DatabaseProvider : ContentProvider() {
    private val bookDir = 0
    private val bookItem = 1
    private val authority = "com.example.abu.listview.databaseproviderdemo.provider"
    private var dbHelper: MyDatabaseHelper? = null
    private val uriMatcher by lazy {
        val mathcer = UriMatcher(UriMatcher.NO_MATCH)
        mathcer.addURI(authority, TABLE_BOOK, bookDir)
        mathcer.addURI(authority, "$TABLE_BOOK/#", bookItem)
        mathcer
    }


    override fun insert(uri: Uri, values: ContentValues?): Uri? = dbHelper?.let {
        val db = it.readableDatabase
        when (uriMatcher.match(uri)) {
            bookDir, bookItem -> {
                val newBookId = db.insert(TABLE_BOOK, null, values)
                Uri.parse("content://$authority/$TABLE_BOOK/$newBookId")
            }
            else -> null
        }
    }

    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor? = dbHelper?.let {
        val db = it.readableDatabase
        when (uriMatcher.match(uri)) {
            bookItem -> {
                //uri.getPathSegments()它会将URI权限之后的部分以 / 进行分割,将分割后的结果方法集合中,那这个集合中第0个位置存放的就是path,第一个位置存放的就是id。
                val queryId = uri.pathSegments[1]
                db.query(TABLE_BOOK, projection, "id=?", arrayOf(queryId), null, null, sortOrder)
            }
            bookDir -> {
                db.query(TABLE_BOOK, projection, selection, selectionArgs, null, null, sortOrder)
            }
            else -> null
        }
    }

    override fun onCreate(): Boolean = context?.let {
        dbHelper = MyDatabaseHelper(it, DB_NAME, version = 1)
        true
    } ?: false

    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?) =
        dbHelper?.let {
            val db = it.readableDatabase
            when (uriMatcher.match(uri)) {
                bookDir -> db.update(TABLE_BOOK, values, selection, selectionArgs)
                bookItem -> {
                    val updateId = uri.pathSegments[1]
                    db.update(TABLE_BOOK, values, "id=?", arrayOf(updateId))
                }
                else -> 0
            }
        } ?: 0

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = dbHelper?.let {
        val db = it.readableDatabase
        when (uriMatcher.match(uri)) {
            bookItem -> {
                val deleteId = uri.pathSegments[1]
                db.delete(TABLE_BOOK, "id=?", arrayOf(deleteId))
            }
            bookDir -> db.delete(TABLE_BOOK, selection, selectionArgs)
            else -> 0
        }
    } ?: 0

    override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) {
        bookDir -> "vnd.android.cursor.dir/vnd.$authority.$TABLE_BOOK"
        bookItem -> "vnd.android.cursor.item/vnd.$authority.$TABLE_BOOK"
        else -> null
    }
}

代码很长,不过并不难理解,一开始定义了两个变量bookDirbookItem用于区分访问Book表的全部数据和单条数据。然后在一个by lazy代码块中对UriMatcher进行初始化操作,并将期望匹配的几种URI添加到UriMatcher中。by lazy是Kotlin中提供的一种懒加载技术,代码块中的代码一开始并不会执行,只有当uriMatcher首次被调用时才会执行,并且将代码块中最后一行代码的返回值赋值给uriMatcher。
接下来就是每个抽象方法的实现了,逻辑比较简单这里就不再一一细说了。现在我们的程序就拥有了跨程序共享数据的功能了,下面看下如何跨程序操作这些数据。新建一个项目,在新项目中执行如下代码。

class MainActivity : AppCompatActivity() ,View.OnClickListener{
    private lateinit var mContentResolver: ContentResolver
    private val authority = "com.example.abu.listview.databaseproviderdemo.provider"
    private val tag = javaClass.simpleName
    private val TABLE_BOOK = "Book"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mContentResolver = contentResolver
        insert_data.setOnClickListener(this)
        delete_data.setOnClickListener(this)
        update_data.setOnClickListener(this)
        query_data.setOnClickListener(this)

    }

    override fun onClick(v: View?) {
        when (v?.id) {
            R.id.insert_data -> {
                val uri = mContentResolver.insert(
                    Uri.parse("content://$authority/$TABLE_BOOK/#"),
                    createValues("name" to "西游记", "pages" to 100000, "author" to "施耐庵")
                )
                Log.e(tag, "插入的新数据的id:${uri!!.pathSegments[1]}")
            }
            R.id.delete_data -> {
                val deleteLine = mContentResolver.delete(Uri.parse("content://$authority/$TABLE_BOOK/3"), null, null)
                Log.e(tag, "删除了几行数据:$deleteLine")
            }
            R.id.update_data -> {
                val updateLine = mContentResolver.update(
                    Uri.parse("content://$authority/$TABLE_BOOK/4"),
                    createValues("name" to "三国演义", "pages" to 200000, "author" to "罗贯中"),
                    null,
                    null
                )
                Log.e(tag, "更新了几行数据:$updateLine")
            }
            R.id.query_data -> {
                val cursor =
                    mContentResolver.query(Uri.parse("content://$authority/$TABLE_BOOK"), null, null, null, null)
                cursor?.apply {
                    while (moveToNext()) {
                        val name = getString(getColumnIndex("name"))
                        val pages = getInt(getColumnIndex("pages"))
                        val author = getString(getColumnIndex("author"))
                        Log.e(tag, "查询:name:$name,pages:$pages,author:$author")
                    }
                    close()
                }

            }
        }

    }
}

功能很简单就是使用ContentProvider对数据进行增删改查。到此为止ContentProvider相关的内容基本就介绍完毕了。

相关文章

网友评论

      本文标题:ContentProvider

      本文链接:https://www.haomeiwen.com/subject/hshuuktx.html