四大组件——ContentProvider

178 阅读10分钟

一、概述

ContentProvider(内容提供者)也是Android的四大组件之一,同样也没有UI交互。ContentProvider可以说是在四大组件中用的最少的一个,它在系统中提供数据管理的功能,允许一个应用访问另一个应用的数据,是Android提供给上层的一个组件,主要用于实现数据访问的统一管理和数据共享,它是Android实现跨程序共享数据的标准。

ContentProvider主要的使用场景有2个:

  1. 自己的应用访问其他应用。比如访问通讯录,就可以对数据进行读取或者修改。

  2. 暴露自己的数据供外界访问。我们可以选择需要暴露的数据,避免隐私数据遭到泄漏。

二、Uri

ContentProvider提供了一系列的方法对数据进行增删查改操作,但是这和在SQLiteDatabase对数据库的增删查改操作有所区别。不同于SQLiteDatabase,ContentProvider中的增删查改方法不接收表名参数,而是使用一个Uri代替,这个uri给ContentProvider中的数据建立了唯一的标识符,它由两部分组成:authorities和path。authorities是用于对不同的应用作区分,是为了避免冲突,所以一般会采用包名+类名(或者provider)的形式;path是用于在同一应用中不同表作区分的,通常都会添加在authorities的后面;并且为了能够让ContentProvider识别,会在头部添加content://协议前缀。比如本例子中的authorities是com.test.contentprovider.basic.use.MyContentProvider,path是book,那么完整的uri就是:content://com.test.contentprovider.basic.use.MyContentProvider/book,因此在uri中就完整的表达出了我们要访问哪个应用程序中的哪张表的数据,所以增删查改方法操作需要接收一个uri对象参数。

使用uri还有一些快捷的用法:

  • content://com.test.contentprovider.basic.use.MyContentProvider/book/1在path路径后面添加一个数字,表示访问book表中id为1的数据。
  • content://com.test.contentprovider.basic.use.MyContentProvider/book/1/name表示访问id为1这条记录的name字段
  • content://com.test.contentprovider.basic.use.MyContentProvider/book表示访问book表的所有数据,这也是我们本文的默认用法。

可以使用通配符格式 *:表示匹配任意长度的任意字符;#:表示匹配任意长度的数字。 那么content://com.test.contentprovider.basic.use.MyContentProvider/*就表示能够匹配任意表的uri; content://com.test.contentprovider.basic.use.MyContentProvider/book/#就表示匹配book表中任意一行数据的uri。

三、ContentProvider的使用

1、自身应用使用

ContentProvider它是一个抽象类,所以不能直接创建它的实例,而是需要我们自定义一个类来继承它。当然,即使我们创建了自定义的类,还需要用到一个类——ContentResolver。ContentProvider共享数据是通过定义一个对外开放的统一的接口来实现的,然而,应用程序并不直接调用这些方法,而是使用一个ContentResolver 对象,调用它的方法作为替代。当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,使用ContentResolver类来完成。在这里我自定义一个MyContentProvider,然后需要重写ContentProvider中的几个方法:onCreate、query、getType、insert、delete、update。对于这几个方法做一下简单的说明。

  • onCreate 用来初始化的时候使用,一般的会在onCreate方法中创建和升级数据库。如果返回true,则表示ContentProvider初始化成功,反之则失败。需要注意的是,只有当ContentResolver对象尝试访问应用中的数据时,ContentProvider才会被初始化。
  • query 在ContentProvider中进行数据查询。通过uri参数对象来确定查询哪张表,query方法中的参数较多,分别说明一下:projection用于查询哪些列,比如select * from book查询所有列、或者select name from book只查询名称这一列;selection指定where的约束条件,比如where name=android查询名称为android的指定值;selectionArgs参数和selection作用相同,都是用来约束where条件的,具体的作用是为where中的占位符提供具体的值,比如当selection参数值是name = ?,那么selectionArgs的值是android,或者其他的具体值;sortOrder指定对查询的结果按哪种方式排序,比如order by name按名称排序、或者order by price按价格排序。根据上面的说明,可以写出一个具体的查询语句select * from book where name = "android" order by name,其实就是SQL语句。最后把返回的结果放在一个Cursor对象中,我们就可以通过Cursor对象的一系列getXxx方法取出列对应的值。
  • update 更新ContentProvider中已经存在的数据。和query方法一样,也是通过uri参数来确定是哪张表,要更新的数据存放在ContentValues对象中,约束条件放在selection、selectionArgs中,和query方法中的作用是一样的不再说明。返回值是受影响的行数。
  • delete 删除ContentProvider中已经存在的数据。通过uri参数确定是哪张表,selection、selectionArgs参数作用同上。返回值是被删除的行数。
  • insert 在ContentProvider中插入数据。通过uri参数确定插入到哪张表中,ContentValues对象参数存放需要插入的数据。返回值表示用来插入这条新数据的uri。
  • getType 根据传入的uri来返回相应的MIME类型。

下面我们还需要自定义一个SQLiteHelper类,来对数据库进行管理。在构造方法中传入了数据库的名称DB_NAME、数据库版本号DB_VERSION,在onCreate方法中创建了一张book表;在onUpgrade用于对数据库的升级。

class MyDbOpenHelper(context: Context):SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION){

    companion object {
        private const val DB_NAME = "my_book.db"
        private const val DB_VERSION = 1
        const val DB_TABLE_BOOK = "book"
    }

    override fun onCreate(db: SQLiteDatabase?) {
        db?.execSQL("CREATE TABLE IF NOT EXISTS $DB_TABLE_BOOK ( id integer primary key autoincrement,name varchar(30),price double ) ")
        println("创建数据库成功")
    }

    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        println("更新数据库成功")
    }
}

接下来是MyContentProvider类

class MyContentProvider : ContentProvider() {

    private lateinit var mDb: SQLiteDatabase
    private lateinit var myDbOpenHelper: MyDbOpenHelper

    override fun onCreate(): Boolean {
        println("MyContentProvider ---> onCreate")
        return true
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        println("MyContentProvider ---> insert")
        myDbOpenHelper = MyDbOpenHelper(MyApp.instance())
        mDb = myDbOpenHelper.writableDatabase
        mDb.insert(MyDbOpenHelper.DB_TABLE_BOOK, null, values)
        context?.contentResolver?.notifyChange(uri, null)
        return uri
    }

    @SuppressLint("Recycle")
    override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
        println("MyContentProvider ---> query")
        myDbOpenHelper = MyDbOpenHelper(MyApp.instance())
        mDb = myDbOpenHelper.readableDatabase
        val cursor : Cursor? = mDb.query(MyDbOpenHelper.DB_TABLE_BOOK, projection, selection, selectionArgs, null, null, sortOrder)
        if(cursor == null){
            println("cursor == null")
        }
        return cursor
    }

    override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
        println("MyContentProvider ---> update")
        mDb = myDbOpenHelper.writableDatabase
        val row = mDb.update(MyDbOpenHelper.DB_TABLE_BOOK, values, selection, selectionArgs)
        if (row > 0) {
            context?.contentResolver?.notifyChange(uri, null)
        }
        return row
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        println("MyContentProvider ---> delete")
        mDb = myDbOpenHelper.writableDatabase
        val count = mDb.delete(MyDbOpenHelper.DB_TABLE_BOOK, selection, selectionArgs)
        if (count > 0) {
            context?.contentResolver?.notifyChange(uri, null)
        }
        return count
    }

    override fun getType(uri: Uri): String? {
        println("MyContentProvider ---> getType")
        return null
    }
}
class MyApp : Application() {

    companion object {
        private var instance: Application? = null
        fun instance() = instance!!
    }
    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}

自定义类写好之后,不要忘记在manifest中配置ContentProvider。在这里需要说明一下provider标签中的两个属性:enabled、exported。android:enabled="true"表示Android系统是否能够实例化该应用程序的组件,所以这里一定是true;android:exported="true"表示该组件是否可以被外部应用调用,如果为false则只能是自己的内部程序启动,所以这里应该设置为true。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.test.contentprovider.basic.use">

    <application
        android:name=".MyApp"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">
        ...
        <provider
            android:name=".MyContentProvider"
            android:authorities="com.test.contentprovider.basic.use.MyContentProvider"
            android:enabled="true"
            android:exported="true" />
        ...
    </application>
</manifest>

最后,在MainActivity创建4个Button,分别用于增删查改操作;创建2个EditText,用来输入书名、单价。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <EditText
        android:id="@+id/book_name_et"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="text"
        android:hint="请输入书名"/>
    <EditText
        android:id="@+id/book_price_et"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="number"
        android:hint="请输入价格"/>
    <Button
        android:id="@+id/insert_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="新增" />
    <Button
        android:id="@+id/query_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="查询" />
    <Button
        android:id="@+id/modify_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="修改数据" />
    <Button
        android:id="@+id/delete_btn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="删除数据" />
</LinearLayout>

在这里需要说明一下MainActivity里4个按钮的逻辑:

  • 首先,是新增按钮,它会在数据库中查询一次输入的书名是否已经存在,如果存在则会新增数据失败,提示书名已存在;如果不存在则新增成功。
  • 其次,是查询按钮,当书名输入框中不为空,则查询的是输入框中的书名;如果书名输入框为空,则查询所有的数据记录;
  • 再次,是修改按钮,修改数据必须要书名输入框以及价格不为空,会先查询输入的书名是否存在,如果存在则会直接修改书名对应的价格;如果不存在则修改失败,会提示不存在此书名。
  • 最后,是删除按钮,当书名输入框不为空时,会删除对应书名的单条记录;如果书名输入框以及价格输入框都为空,则会删除所有记录。

当然,虽然4个按钮的判断逻辑并不全面,本例中只是用来做学习使用的,就不用钻牛角尖了。下面是MainActivity的全部代码,先贴出来,后面再逐个分析。

class MainActivity : AppCompatActivity(), View.OnClickListener {

    private var insertBtn: Button? = null
    private var queryBtn: Button? = null
    private var modifyBtn: Button? = null
    private var deleteBtn: Button? = null
    private var bookNameEt: EditText? = null
    private var bookPriceEt: EditText? = null

    private val MY_PERMISSIONS_REQUEST = 1

    private lateinit var myUri: Uri
    private lateinit var contentResolver : MyContentProvider

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        bookNameEt = findViewById(R.id.book_name_et)
        bookPriceEt = findViewById(R.id.book_price_et)
        insertBtn = findViewById(R.id.insert_btn)
        queryBtn = findViewById(R.id.query_btn)
        modifyBtn = findViewById(R.id.modify_btn)
        deleteBtn = findViewById(R.id.delete_btn)
        insertBtn?.setOnClickListener(this)
        queryBtn?.setOnClickListener(this)
        modifyBtn?.setOnClickListener(this)
        deleteBtn?.setOnClickListener(this)

        requestPermission()

        contentResolver = MyContentProvider()
        myUri = Uri.parse("content://com.test.contentprovider.basic.use.MyContentProvider/${MyDbOpenHelper.DB_TABLE_BOOK}")
    }

    private fun requestPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
                        MY_PERMISSIONS_REQUEST)
            } else {
                ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
                        MY_PERMISSIONS_REQUEST)
            }
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        if (requestCode == MY_PERMISSIONS_REQUEST) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                println("已授权")
            } else {
                showToast("未授予相关权限,无法使用本应用")
                Handler().postDelayed({
                    finish()
                }, 1000)
            }
        }
    }

    @SuppressLint("Recycle")
    override fun onClick(v: View?) {
        val nameInput = bookNameEt?.text.toString()
        val priceInput = bookPriceEt?.text.toString()
        when (v) {
            insertBtn -> {
                if (TextUtils.isEmpty(nameInput)) {
                    showToast("请输入名称")
                    return
                }
                if (TextUtils.isEmpty(priceInput)) {
                    showToast("请输入价格")
                    return
                }

                val cursor: Cursor? = contentResolver.query(myUri, null, "name = ?", arrayOf(nameInput), null)
                cursor?.let {
                    if (it.moveToNext()) {
                        showToast("已经包含了该书,请重新输入")
                        return
                    }
                }
                val contentValue1 = ContentValues()
                contentValue1.put("name", nameInput)
                contentValue1.put("price", priceInput.toDouble())
                contentResolver.insert(myUri, contentValue1)
                toShow("插入数据成功")

                cursor?.close()
            }
            queryBtn -> {
                val queryCursor: Cursor? = if (TextUtils.isEmpty(nameInput) && TextUtils.isEmpty(priceInput)) {
                    //查询全部数据
                    contentResolver.query(myUri, arrayOf("id", "name", "price"), null, null, null)
                } else {
                    //根据书名查询
                    if (TextUtils.isEmpty(nameInput)) {
                        showToast("请输入名称")
                        return
                    }
                    contentResolver.query(myUri, arrayOf("id", "name", "price"), "name = ?", arrayOf(nameInput), null)
                }
                if(queryCursor == null){
                    toShow("没有查询到相关数据")
                }else{
                    if(queryCursor.count > 0){
                        while (queryCursor.moveToNext()) {
                            val id = queryCursor.getInt(0)
                            val name = queryCursor.getString(1)
                            val price = queryCursor.getDouble(2)
                            println("id:$id    name:$name    price:$price")
                        }
                    }else{
                        toShow("没有查询到相关数据")
                    }
                }
                queryCursor?.close()
            }
            modifyBtn -> {
                if (TextUtils.isEmpty(nameInput)) {
                    showToast("请输入名称")
                    return
                }
                if (TextUtils.isEmpty(priceInput)) {
                    showToast("请输入价格")
                    return
                }
                val cursor: Cursor? = contentResolver.query(myUri, arrayOf("name"), "name = ?", arrayOf(nameInput), null)
                if (cursor == null) {
                    toShow("没有查询的需要修改的书名")
                } else {
                    if(cursor.count > 0){
                        val contentValues = ContentValues()
                        contentValues.put("name", nameInput)
                        contentValues.put("price", priceInput.toDouble())
                        val updateCount = contentResolver.update(myUri, contentValues, "name = ?", arrayOf(nameInput))
                        if (updateCount > 0) {
                            toShow("数据更新成功")
                        } else {
                            toShow("数据更新失败")
                        }
                    }else{
                        toShow("没有查询的需要修改的书名")
                    }
                }
                cursor?.close()
            }
            deleteBtn -> {
                val deleteCount: Int
                val deleteMsg = if (TextUtils.isEmpty(nameInput) && TextUtils.isEmpty(priceInput)) {
                    deleteCount = contentResolver.delete(myUri, null, null)
                    if (deleteCount > 0) {
                        "删除全部数据成功"
                    } else {
                        "删除全部数据失败"
                    }
                } else {
                    deleteCount = contentResolver.delete(myUri, "name = ?", arrayOf(nameInput))
                    if (deleteCount > 0) {
                        "删除指定书名数据成功"
                    } else {
                        "删除指定书名数据失败"
                    }
                }
                toShow(deleteMsg)
            }
        }
    }
    
    private fun toShow(msg: String){
        showToast(msg)
        println(msg)
    }

    private fun showToast(msg: String) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
    }
}
  1. 动态权限申请。因为本文涉及到数据库的操作所以必须要给予授权,否则无法使用本应用,所以在Android 6.0以上版本需要做动态权限申请,6.0以下可以无视。涉及到的危险权限是android.permission.READ_EXTERNAL_STORAGE读取外部存储,这里主要是查询数据;android.permission.WRITE_EXTERNAL_STORAGE写入外部存储,这里主要是新增/修改数据。具体使用这里不再过多说明,可以参考其他相应的文章。
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
    if (requestCode == MY_PERMISSIONS_REQUEST) {
        if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            println("已授权")
        } else {
            showToast("未授予相关权限,无法使用本应用")
            Handler().postDelayed({
                finish()
            }, 1000)
        }
    }
}

从代码中可以看到,如果用户未授权的话,延迟1s就会finish掉,无法使用本应用。对于这两个危险权限来说,是由于本应用必须要用到的权限,就算不直接finish掉,当我们点击任何按钮,都会导致crash,没有任何意义。

  1. 初始化uri。可以看到我们在onCreate中延迟初始化了uri,其实在哪里初始化都可以,但前提是在执行增删查改操作之前初始化uri,因为增删查改的所有操作都是基于这个uri对象的,具体说明前面已经说过。
myUri = Uri.parse("content://com.test.contentprovider.basic.use.MyContentProvider/${MyDbOpenHelper.DB_TABLE_BOOK}")
  1. 新增数据。当完成了uri的初始化之后,就可以进行增删查改操作了,只不过现在还没有任何的数据,所以我们必须要先新增一些数据进去。
...
val cursor: Cursor? = contentResolver.query(myUri, null, "name = ?", arrayOf(nameInput), null)
cursor?.let {
    if (it.moveToNext()) {
        showToast("已经包含了该书,请重新输入")
        return
    }
}
val contentValue1 = ContentValues()
contentValue1.put("name", nameInput)
contentValue1.put("price", priceInput.toDouble())
contentResolver.insert(myUri, contentValue1)
toShow("插入数据成功")

cursor?.close()
...

在插入数据之前先进行了查询操作,当已经存在的数据,就直接返回了;当不存在就直接插入。查询操作里的参数之前已经说过了,就是通过书名占位符,然后通过输入的具体值进行替换通配符。把需要插入的数据放到ContentValues对象中,其实就是一个HashMap<String, Object>对象。最后调用自定义的MyContentProvider中的insert方法把数据插入到数据库中,并将插入数据的uri进行返回。在输入框中输入相关数据后,我们插入几条数据后,点击查询按钮,可以看到我们插入的数据。

从上图中我们可以看到,插入了4条数据,在插入数据之前,先执行了查询操作。

  1. 查询数据。从下面的代码逻辑可以看到,判断了输入框中是否有值,若为空则查询所有数据,否则查询指定书名的数据。
val queryCursor: Cursor? = if (TextUtils.isEmpty(nameInput) && TextUtils.isEmpty(priceInput)) {
    //查询全部数据
    contentResolver.query(myUri, arrayOf("id", "name", "price"), null, null, null)
} else {
    //根据书名查询
    if (TextUtils.isEmpty(nameInput)) {
        showToast("请输入名称")
        return
    }
    contentResolver.query(myUri, arrayOf("id", "name", "price"), "name = ?", arrayOf(nameInput), null)
}
if(queryCursor == null){
    toShow("没有查询到相关数据")
}else{
    if(queryCursor.count > 0){
        while (queryCursor.moveToNext()) {
            val id = queryCursor.getInt(0)
            val name = queryCursor.getString(1)
            val price = queryCursor.getDouble(2)
            println("id:$id    name:$name    price:$price")
        }
    }else{
        toShow("没有查询到相关数据")
    }
}
queryCursor?.close()

查询前面已经提到了多次,会返回一个Cursor?对象,我们就可以直接判断是否为空以及getCount()方法是否大于0。并循环判断Cursor对象的moveToNext方法是否已经移到数据集的最后一个,如果已经是最后一个,此方法会返回false,表示已经没有数据了。在循环体中通过调用Cursor对象的getXxx方法,我们这里是调用getXxx(int columnIndex)方法,里面的参数表示的是表中列的下标,因为我们的id是第一列,并且是integer类型的,所以我们取的是queryCursor.getInt(0),名称是第二列,并且是varchar类型的,所以我们取的是queryCursor.getString(1),以此类推。需要注意的是,一次循环表示一条数据记录。

当我们不输入任何数据时,查询所有数据,打印以下输出。

当我们在书名输入框中输入Android后,再点击查询按钮,打印以下输出。可以看到,就只查询出了一条数据。

  1. 修改数据。修改的逻辑前面已经说过,必须输入框中都有值,然后通过输入的书名来判断数据库中是否有记录。
...
val cursor: Cursor? = contentResolver.query(myUri, arrayOf("name"), "name = ?", arrayOf(nameInput), null)
if (cursor == null) {
    toShow("没有查询的需要修改的书名")
} else {
    if(cursor.count > 0){
        val contentValues = ContentValues()
        contentValues.put("name", nameInput)
        contentValues.put("price", priceInput.toDouble())
        val updateCount = contentResolver.update(myUri, contentValues, "name = ?", arrayOf(nameInput))
        if (updateCount > 0) {
            toShow("数据更新成功")
        } else {
            toShow("数据更新失败")
        }
    }else{
        toShow("没有查询的需要修改的书名")
    }
}
cursor?.close()
...

还是先查询了数据是否存在,并返回一个Cursor?对象,同第4点的查询一样,会返回一个Cursor?对象。把需要修改的数据放到ContentValues中,调用contentResolver.update方法,这个方法会返回修改数据后受影响的行数。我们就可以通过这个行数是否大于0来判断是否修改成功。

我们修改ios的价格为22,点击修改按钮,再点击查询按钮后,得到以下输出。

  1. 删除数据。
val deleteCount: Int
val deleteMsg = if (TextUtils.isEmpty(nameInput) && TextUtils.isEmpty(priceInput)) {
    deleteCount = contentResolver.delete(myUri, null, null)
    if (deleteCount > 0) {
        "删除全部数据成功"
    } else {
        "删除全部数据失败"
    }
} else {
    deleteCount = contentResolver.delete(myUri, "name = ?", arrayOf(nameInput))
    if (deleteCount > 0) {
        "删除指定书名数据成功"
    } else {
        "删除指定书名数据失败"
    }
}
toShow(deleteMsg)

我们在书名输入框中输入c#,点击删除按钮后,再次点击查询按钮,得到以下输出。可以看到成功删除了c#这本书。

调用MyContentProvider中的delete方法,会返回受影响的行数。我们就可以通过这个行数值是否不为0来判断删除成功。删除操作应该是这几个中最简单的了。

2、外部应用使用

我们称上面的那个应用为主应用,新建另外一个项目称之为辅应用,用来访问主应用中所暴露的数据,主应用的包名是com.test.contentprovider.basic.use,辅应用的包名是com.test.contentprovider.use.other。辅应用项目中只有1个布局文件和1个MainActivity,非常简单。布局文件和上一个项目几乎是一样的,也是2个输入框和4个按钮,只是在头部添加了一个TextView为了区别于主应用项目,布局文件就不贴了。在MainActivity中按钮的用法也是基本相同的,可以看到我们在4个按钮的点击事件中先打印出了相关操作的说明。接下来还是逐个分析一下。

class MainActivity : AppCompatActivity(),View.OnClickListener {

    private var insertBtn: Button? = null
    private var queryBtn: Button? = null
    private var modifyBtn: Button? = null
    private var deleteBtn: Button? = null
    private var bookNameEt: EditText? = null
    private var bookPriceEt: EditText? = null

    private val MY_PERMISSIONS_REQUEST = 1

    private val myUri: Uri = Uri.parse("content://com.test.contentprovider.basic.use.MyContentProvider/book")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        bookNameEt = findViewById(R.id.book_name_et)
        bookPriceEt = findViewById(R.id.book_price_et)
        insertBtn = findViewById(R.id.insert_btn)
        queryBtn = findViewById(R.id.query_btn)
        modifyBtn = findViewById(R.id.modify_btn)
        deleteBtn = findViewById(R.id.delete_btn)
        insertBtn?.setOnClickListener(this)
        queryBtn?.setOnClickListener(this)
        modifyBtn?.setOnClickListener(this)
        deleteBtn?.setOnClickListener(this)

        requestPermission()
    }

    private fun requestPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
            if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
                        MY_PERMISSIONS_REQUEST)
            } else {
                ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE),
                        MY_PERMISSIONS_REQUEST)
            }
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
        if (requestCode == MY_PERMISSIONS_REQUEST) {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                println("已授权")
            } else {
                showToast("未授予相关权限,无法使用本应用")
                Handler().postDelayed({
                    finish()
                }, 1000)
            }
        }
    }

    @SuppressLint("Recycle")
    override fun onClick(v: View?) {
        val nameInput = bookNameEt?.text.toString()
        val priceInput = bookPriceEt?.text.toString()
        when (v) {
            insertBtn -> {
                println("插入数据到三方应用")
                if (TextUtils.isEmpty(nameInput)) {
                    showToast("请输入名称")
                    return
                }
                if (TextUtils.isEmpty(priceInput)) {
                    showToast("请输入价格")
                    return
                }

                val cursor: Cursor? = contentResolver.query(myUri, null, "name = ?", arrayOf(nameInput), null)
                cursor?.let {
                    if (it.moveToNext()) {
                        showToast("已经包含了该书,请重新输入")
                        return
                    }
                }
                val contentValue1 = ContentValues()
                contentValue1.put("name", nameInput)
                contentValue1.put("price", priceInput.toDouble())
                contentResolver.insert(myUri, contentValue1)
                println("插入数据成功")

                cursor?.close()
            }
            queryBtn -> {
                println("查询三方应用的数据")
                val queryCursor: Cursor? = if (TextUtils.isEmpty(nameInput) && TextUtils.isEmpty(priceInput)) {
                    //查询全部数据
                    contentResolver.query(myUri, null, null, null, null)
                } else {
                    //根据书名查询
                    if (TextUtils.isEmpty(nameInput)) {
                        showToast("请输入名称")
                        return
                    }
                    contentResolver.query(myUri, null, "name = ?", arrayOf(nameInput), null)
                }
                if(queryCursor == null){
                    toShow("没有查询到相关数据")
                }else{
                    if(queryCursor.count > 0){
                        while (queryCursor.moveToNext()) {
                            val id = queryCursor.getInt(0)
                            val name = queryCursor.getString(1)
                            val price = queryCursor.getDouble(2)
                            println("id:$id    name:$name    price:$price")
                        }
                    }else{
                        toShow("没有查询到相关数据")
                    }
                }
            }
            modifyBtn -> {
                println("修改三方应用的数据")
                if (TextUtils.isEmpty(nameInput)) {
                    showToast("请输入名称")
                    return
                }
                if (TextUtils.isEmpty(priceInput)) {
                    showToast("请输入价格")
                    return
                }
                val cursor: Cursor? = contentResolver.query(myUri, arrayOf("name"), "name = ?", arrayOf(nameInput), null)
                if (cursor == null) {
                    toShow("没有查询的需要修改的书名")
                } else {
                    if(cursor.count > 0){
                        val contentValues = ContentValues()
                        contentValues.put("name", nameInput)
                        contentValues.put("price", priceInput.toDouble())
                        val updateCount = contentResolver.update(myUri, contentValues, "name = ?", arrayOf(nameInput))
                        if (updateCount > 0) {
                            toShow("数据更新成功")
                        } else {
                            toShow("数据更新失败")
                        }
                    }else{
                        toShow("没有查询的需要修改的书名")
                    }
                }
            }
            deleteBtn -> {
                println("删除三方应用的数据")
                val deleteCount: Int
                val deleteMsg = if (TextUtils.isEmpty(nameInput) && TextUtils.isEmpty(priceInput)) {
                    deleteCount = contentResolver.delete(myUri, null, null)
                    if (deleteCount > 0) {
                        "删除全部数据成功"
                    } else {
                        "删除全部数据失败"
                    }
                } else {
                    deleteCount = contentResolver.delete(myUri, "name = ?", arrayOf(nameInput))
                    if (deleteCount > 0) {
                        "删除指定书名数据成功"
                    } else {
                        "删除指定书名数据失败"
                    }
                }
                toShow(deleteMsg)
            }
        }
    }

    private fun toShow(msg: String){
        showToast(msg)
        println(msg)
    }

    private fun showToast(msg: String) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
    }
}
  1. 动态权限的申请。由于要访问其他应用数据库中的数据,所以文件访问的权限必须要先获取到,在Android 6.0以上版本需要动态申请权限,由于我们的项目是必须要这两个权限的,这里的用法是和之前一样的,不再赘述。
  2. 初始化uri。可以看到我们在定义的时候就赋值了,要访问其他应用共享出来的数据时,必须要先知道暴露出来的uri,我们就可以知道是哪个应用中的哪张数据表。后面的增删查改操作就是基于这个uri之上的。
  3. 查询数据。我们这里先直接查询一下全部数据,其他应用暴露出来的数据有哪些,点击查询按钮,得到以下输出。
    从图中打印输出程序的包名看出,我们查询到了主应用的所有数据成功,之前我们删除了c#这条记录,所以查询到的结果和之前是一样的。在所有操作之前,我们先要通过getContentResolver()方法获取到ContentResolver对象,因为之前也说过对ContentProvider的所有操作都需要通过ContentResolver对象来完成,下面的新增、修改、删除操作也是一样。 在输入框中输入Android单独查询,得到以下输出。可以看到也是查询成功了
  4. 新增数据。新增操作和主应用一样,直接在输入框中输入java以及价格70,点击插入按钮之后,我们再次点击查询按钮查询所有的数据,得到以下输出。可以看到数据成功插入。
  5. 修改数据。修改操作和主应用一样,对刚刚添加的java数据的价格进行修改到75,点击修改按钮,再次点击查询所有数据,得到以下输出。可以看到修改数据成功。
  6. 删除数据。删除数据和主应用一样。在输入框中输入java,点击删除按钮,再次点击查询按钮查询所有数据,得到以下输出。可以看到数据删除成功。

四、总结

内容提供者ContentProvider,它是Android的四大组件之一,提供了不同应用之间进行数据通信的功能。虽然这个组件相较于其他三个的用的不多,但是在功能上ContentProvider主要是体现在数据的共享,比如联系人、日历等。对于一般的App来说,也出于安全的因素,不会把自己的数据暴露给别的App。但是自己的App内部使用的话,一般都是直接使用数据库或者相关框架,也不会使用ContentProvider。最后来说,毕竟ContentProvider是四大组件之一,还是应该知道相关的使用。

主应用代码地址:github.com/leewell5717…

辅应用代码地址:github.com/leewell5717…

五、参考

ContentProvider基本使用

ContentProvider使用场景解读

ContentResolver(一)

第一行代码(第二版)