一、概述
ContentProvider(内容提供者)也是Android的四大组件之一,同样也没有UI交互。ContentProvider可以说是在四大组件中用的最少的一个,它在系统中提供数据管理的功能,允许一个应用访问另一个应用的数据,是Android提供给上层的一个组件,主要用于实现数据访问的统一管理和数据共享,它是Android实现跨程序共享数据的标准。
ContentProvider主要的使用场景有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()
}
}
- 动态权限申请。因为本文涉及到数据库的操作所以必须要给予授权,否则无法使用本应用,所以在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,没有任何意义。
- 初始化uri。可以看到我们在onCreate中延迟初始化了uri,其实在哪里初始化都可以,但前提是在执行增删查改操作之前初始化uri,因为增删查改的所有操作都是基于这个uri对象的,具体说明前面已经说过。
myUri = Uri.parse("content://com.test.contentprovider.basic.use.MyContentProvider/${MyDbOpenHelper.DB_TABLE_BOOK}")
- 新增数据。当完成了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条数据,在插入数据之前,先执行了查询操作。
- 查询数据。从下面的代码逻辑可以看到,判断了输入框中是否有值,若为空则查询所有数据,否则查询指定书名的数据。
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后,再点击查询按钮,打印以下输出。可以看到,就只查询出了一条数据。
- 修改数据。修改的逻辑前面已经说过,必须输入框中都有值,然后通过输入的书名来判断数据库中是否有记录。
...
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,点击修改按钮,再点击查询按钮后,得到以下输出。
- 删除数据。
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#这本书。
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()
}
}
- 动态权限的申请。由于要访问其他应用数据库中的数据,所以文件访问的权限必须要先获取到,在Android 6.0以上版本需要动态申请权限,由于我们的项目是必须要这两个权限的,这里的用法是和之前一样的,不再赘述。
- 初始化uri。可以看到我们在定义的时候就赋值了,要访问其他应用共享出来的数据时,必须要先知道暴露出来的uri,我们就可以知道是哪个应用中的哪张数据表。后面的增删查改操作就是基于这个uri之上的。
- 查询数据。我们这里先直接查询一下全部数据,其他应用暴露出来的数据有哪些,点击查询按钮,得到以下输出。
从图中打印输出程序的包名看出,我们查询到了主应用的所有数据成功,之前我们删除了c#这条记录,所以查询到的结果和之前是一样的。在所有操作之前,我们先要通过getContentResolver()方法获取到ContentResolver对象,因为之前也说过对ContentProvider的所有操作都需要通过ContentResolver对象来完成,下面的新增、修改、删除操作也是一样。 在输入框中输入Android单独查询,得到以下输出。可以看到也是查询成功了 - 新增数据。新增操作和主应用一样,直接在输入框中输入java以及价格70,点击插入按钮之后,我们再次点击查询按钮查询所有的数据,得到以下输出。可以看到数据成功插入。
- 修改数据。修改操作和主应用一样,对刚刚添加的java数据的价格进行修改到75,点击修改按钮,再次点击查询所有数据,得到以下输出。可以看到修改数据成功。
- 删除数据。删除数据和主应用一样。在输入框中输入java,点击删除按钮,再次点击查询按钮查询所有数据,得到以下输出。可以看到数据删除成功。
四、总结
内容提供者ContentProvider,它是Android的四大组件之一,提供了不同应用之间进行数据通信的功能。虽然这个组件相较于其他三个的用的不多,但是在功能上ContentProvider主要是体现在数据的共享,比如联系人、日历等。对于一般的App来说,也出于安全的因素,不会把自己的数据暴露给别的App。但是自己的App内部使用的话,一般都是直接使用数据库或者相关框架,也不会使用ContentProvider。最后来说,毕竟ContentProvider是四大组件之一,还是应该知道相关的使用。
主应用代码地址:github.com/leewell5717…
辅应用代码地址:github.com/leewell5717…
五、参考
第一行代码(第二版)