android 之跨程序共享数据 ContentProvider

ContentProvider

Android数据持久化技术,包括文件存储、SharedPreferences存储以及数据库存储,这些持久化技术所保存的数据只能在当前应用程序中访问,ContentProvider 可以解决这个问题

ContentProvider 主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。ContentProvider可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险

Android运行时权限

Android现在将常用的权限大致归成了两类,一类是普通权限,一类是危险权限

普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,对于这部分权限申请,系统会自动帮我们进行授权,不需要用户手动操作

危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等,对于这部分权限申请,必须由用户手动授权才可以,否则程序就无法使用相应的功能,下面是到Android 10系统为止所有的危险权限


image-20211209173718001.png

每个危险权限都属于一个权限组,我们在进行运行时权限处理时使用的是权限名。原则上,用户一旦同意了某个权限申请之后,同组的其他权限也会被系统自动授权

每当要使用一个权限时,如果是这张表中的权限,就需要进行运行时权限处理,否则,只需要在 AndroidManifest.xml 文件中添加一下权限声明

运行时申请权限

下面是直接打电话,并进行 运行时申请权限 的示例

在 AndroidManifest.xml 中声明权限

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

代码

class MainActivity : AppCompatActivity() {
    companion object {
        private const val TAG = "MainActivity"
        private const val CALL_PHONE_REQUEST_CODE = 6
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_contentprovider_main)
        make_call.setOnClickListener {
            // 检查权限
            if (ContextCompat.checkSelfPermission(
                    applicationContext,
                    Manifest.permission.CALL_PHONE
                ) == PackageManager.PERMISSION_DENIED
            ) {
                // 没有权限,请求权限
                ActivityCompat.requestPermissions(
                    this,
                    arrayOf(Manifest.permission.CALL_PHONE),
                    CALL_PHONE_REQUEST_CODE
                )
            } else {
                call()
            }
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            CALL_PHONE_REQUEST_CODE -> {
                if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
                    // 被授权了,直接打电话
                    call()
                } else {
                    Toast.makeText(this, "you denied the permission", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    private fun call() {
        val intent = Intent(Intent.ACTION_CALL)
        intent.data = Uri.parse("tel:10086")
        startActivity(intent)
    }
}

访问其他程序中的数据

ContentProvider的用法一般有两种:一种是使用现有的ContentProvider读取和操作相应程序中的数据;另一种是创建自己的ContentProvider,给程序的数据提供外部访问接口

如果一个应用程序通过ContentProvider对其数据提供了外部访问接口,那么任何其他的应用程序都可以对这部分数据进行访问。Android系统中自带的通讯录、短信、媒体库等程序都提供了类似的访问接口

ContentResolver的基本用法

想要访问ContentProvider中共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法获取该类的实例。

ContentResolver中提供了一系列的方法用于对数据进行增删改查操作:insert()、update()、delete()、query()

查询数据

ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。内容URI给ContentProvider中的数据建立了唯一标识符,它主要由两部分组成:authority和path。authority是用于对不同的应用程序做区分的,一般为了避免冲突,会使用应用包名,例如:com.example.app.provider。path则是用于对同一应用程序中不同的表做区分的。再在头部加上协议声明 content://,就是一个URI字符串了。例如:

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

在得到了内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入进行查询

val uri = Uri.parse("content://com.example.app.provider/table1")
contentResolver.query(uri,null, null, null, null)

下面是query方法的参数说明


image-20211209201312661.png

查询完成后返回的仍然是一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取出来了

while (cursor.moveToNext()) {
    val column1 = cursor.getString(cursor.getColumnIndex("column1"))
    val column2 = cursor.getString(cursor.getColumnIndex("column2"))
}
cursor.close()

添加数据

val values = contentValuesOf("column1" to "text", "column2" to 1)
contentResolver.insert(uri,values)

更新数据

val values = contentValuesOf("column1" to "text", "column2" to 1)
contentResolver.update(uri, values, "column1= ? and column2 = ?", arrayOf("text", "1"))

删除数据

contentResolver.delete(uri, "column2 = ?", arrayOf("1"))

读取系统联系人

在 AndroidManifest.xml 中声明权限

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

代码

class MainActivity : AppCompatActivity() {
    companion object {
        private const val TAG = "MainActivity"
        private const val READ_CONTACTS_REQUEST_CODE = 7
    }

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

        read_contacts.setOnClickListener {
            // 检查权限
            if (ContextCompat.checkSelfPermission(
                    applicationContext,
                    Manifest.permission.READ_CONTACTS
                ) == PackageManager.PERMISSION_DENIED
            ) {
                // 没有权限,请求权限
                ActivityCompat.requestPermissions(
                    this,
                    arrayOf(Manifest.permission.READ_CONTACTS),
                    CALL_PHONE_REQUEST_CODE
                )
            } else {
                readContacts()
            }
        }
    }

    /**
     * 读取联系人
     */
    private fun readContacts() {
        contentResolver.query(
            ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
            null,
            null,
            null,
            null
        )?.apply {
            while (moveToNext()) {
                val name =
                    getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME))
                val number =
                    getString(getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER))
                Log.d(TAG, "readContacts: name=$name number=$number")
            }
            close()
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            READ_CONTACTS_REQUEST_CODE -> {
                if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
                    // 被授权了,读取联系人
                    readContacts()
                } else {
                    Toast.makeText(this, "you denied the permission", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

}

创建自己的ContentProvider

如果想要实现跨程序共享数据的功能,可以通过新建一个类去继承ContentProvider的方式来实现

class MyProvider : ContentProvider() {

    /**
     * 初始化ContentProvider的时候调用
     * 通常会在这里完成对数据库的创建和升级等操作
     * 返回true表示ContentProvider初始化成功,返回false则表示失败
     * @return Boolean
     */
    override fun onCreate(): Boolean {
    }

    /**
     * 从ContentProvider中查询数据
     *
     * @param uri 参数用于确定查询哪张表
     * @param projection 参数用于确定查询哪些列
     * @param selection 用于约束查询哪些行
     * @param selectionArgs 用于约束查询哪些行
     * @param sortOrder 参数用于对结果进行排序,
     * @return 查询的结果存放在Cursor对象中返回
     */
    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
    }

    /**
     * 根据传入的内容URI返回相应的MIME类型
     * @param uri Uri
     * @return String?
     */
    override fun getType(uri: Uri): String? {
    }

    /**
     * 向ContentProvider中添加一条数据
     *
     * @param uri 用于确定要添加到的表
     * @param values 待添加的数据保存在values参数中
     * @return 返回一个用于表示这条新记录的URI
     */
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
    }

    /**
     * 从ContentProvider中删除数据
     *
     * @param uri 用于确定删除哪一张表中的数据
     * @param selection 用于约束删除哪些行
     * @param selectionArgs 用于约束删除哪些行
     * @return Int 被删除的行数将作为返回值返回
     */
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
    }

    /**
     * 更新ContentProvider中已有的数据
     *
     * @param uri 用于确定更新哪一张表中的数据
     * @param values 新数据保存在values参数中
     * @param selection 用于约束更新哪些行
     * @param selectionArgs 用于约束更新哪些行
     * @return Int 受影响的行数将作为返回值返回
     */
    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
    }
}

URI格式还可以加上一个id

// 表示table1表中id为1的数据
content://com.example.app.provider/table1/1

以路径结尾表示期望访问该表中所有的数据,以id结尾表示期望访问该表中拥有相应id的数据,可以使用通配符分别匹配这两种格式的内容URI

  1. * 表示匹配任意长度的任意字符
  2. # 表示匹配任意长度的数字

个能够匹配任意表的内容URI格式

content://com.example.app.provider/*

一个能够匹配table1表中任意一行数据的内容URI格式

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

借助UriMatcher这个类就可以轻松地实现匹配内容URI的功能。UriMatcher中提供了一个addURI()方法,这个方法接收3个参数,可以分别把authority、path和一个自定义代码传进去。这样,当调用UriMatcher的match()方法时,就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了

class MyProvider : ContentProvider() {
    companion object {
        private const val TAG = "MyProvider"
        private const val TABLE_BOOK_DIR = 0
        private const val TABLE_BOOK_ITEM = 1
        private const val TABLE_CATEGORY_DIR = 2
        private const val TABLE_CATEGORY_ITEM = 3
        private const val AUTHORITY = "com.example.androidstudy.contentprovider_study"
    }

    private val uriMatcher by lazy {
        UriMatcher(UriMatcher.NO_MATCH).apply {
            addURI(AUTHORITY, "Book", TABLE_BOOK_DIR)
            addURI(AUTHORITY, "Book/#", TABLE_BOOK_ITEM)
            addURI(AUTHORITY, "Category", TABLE_CATEGORY_DIR)
            addURI(AUTHORITY, "Category/#", TABLE_CATEGORY_ITEM)
        }
    }

    /**
     * 初始化ContentProvider的时候调用
     * 通常会在这里完成对数据库的创建和升级等操作
     * 返回true表示ContentProvider初始化成功,返回false则表示失败
     * @return Boolean
     */
    override fun onCreate(): Boolean {
    }

    /**
     * 从ContentProvider中查询数据
     *
     * @param uri 参数用于确定查询哪张表
     * @param projection 参数用于确定查询哪些列
     * @param selection 用于约束查询哪些行
     * @param selectionArgs 用于约束查询哪些行
     * @param sortOrder 参数用于对结果进行排序,
     * @return 查询的结果存放在Cursor对象中返回
     */
    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        when (uriMatcher.match(uri)) {
            TABLE_BOOK_DIR -> {
                //查询table1的所有数据
            }
            TABLE_BOOK_ITEM -> {
                //查询table1的单条数据
            }
            TABLE_CATEGORY_DIR -> {
                //查询tabl2e1的所有数据
            }
            TABLE_CATEGORY_ITEM -> {
                //查询table2的单条数据
            }
        }
    }

    /**
     * 根据传入的内容URI返回相应的MIME类型
     * @param uri Uri
     * @return String?
     */
    override fun getType(uri: Uri): String? {
        return when (uriMatcher.match(uri)) {
            TABLE_BOOK_DIR -> {
                "vnd.android.cursor.dir/$AUTHORITY.Book"
            }
            TABLE_BOOK_ITEM -> {
                "vnd.android.cursor.item/$AUTHORITY.Book"
            }
            TABLE_CATEGORY_DIR -> {
                "vnd.android.cursor.dir/$AUTHORITY.Category"
            }
            TABLE_CATEGORY_ITEM -> {
                "vnd.android.cursor.item/$AUTHORITY.Category"
            }
            else -> null
        }
    }

    /**
     * 向ContentProvider中添加一条数据
     *
     * @param uri 用于确定要添加到的表
     * @param values 待添加的数据保存在values参数中
     * @return 返回一个用于表示这条新记录的URI
     */
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
    }

    /**
     * 从ContentProvider中删除数据
     *
     * @param uri 用于确定删除哪一张表中的数据
     * @param selection 用于约束删除哪些行
     * @param selectionArgs 用于约束删除哪些行
     * @return Int 被删除的行数将作为返回值返回
     */
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
    }

    /**
     * 更新ContentProvider中已有的数据
     *
     * @param uri 用于确定更新哪一张表中的数据
     * @param values 新数据保存在values参数中
     * @param selection 用于约束更新哪些行
     * @param selectionArgs 用于约束更新哪些行
     * @return Int 受影响的行数将作为返回值返回
     */
    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
    }
}

insert()、update()、delete()这几个方法的实现是差不多的,它们都会携带uri这个参数,然后同样利用UriMatcher的match()方法判断出调用方期望访问的是哪张表,再对该表中的数据进行相应的操作就可以了

getType()方法是所有的ContentProvider都必须提供的一个方法,用于获取Uri对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成

  1. 如果内容URI以路径结尾,则开头为vnd.android.cursor.dir/;如果内容URI以id结尾,则开头为vnd.android.cursor.item/
  2. 然后接上vnd.<authority>.<path>

示例

内容URI MIME类型
content://com.example.app.provider/table1 vnd.android.cursor.dir/vnd.com.example.app.provider.table1
content://com.example.app.provider/table1/1 vnd.android.cursor.item/vnd.com.example.app.provider.table1

因此,MyProvider 的 getType 方法可改为如下

override fun getType(uri: Uri): String? {
    return when (uriMatcher.match(uri)) {
        TABLE_1_DIR -> {
            "vnd.android.cursor.dir/vnd.com.example.androidstudy.table1"
        }
        TABLE_1_ITEM -> {
            "vnd.android.cursor.item/vnd.com.example.androidstudy.table1"
        }
        TABLE_2_DIR -> {
            "vnd.android.cursor.dir/vnd.com.example.androidstudy.table2"
        }
        TABLE_2_ITEM -> {
            "vnd.android.cursor.item/vnd.com.example.androidstudy.table2"
        }
        else -> null
    }
}

因为所有的增删改查操作都一定要匹配到相应的内容URI格式才能进行,而我们当然不可能向UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问,安全问题也就不存在了

实现跨程序数据共享

在 AndroidManifest.xml 文件中配置 provider

<provider
    android:name=".contentprovider_study.MyProvider"
    android:authorities="com.example.androidstudy.contentprovider_study.provider"
    android:enabled="true"
    android:exported="true">
    <!--android:name属性指定了DatabaseProvider的类名
    android:authorities属性指定了DatabaseProvider的authority
    Exported属性表示是否允许外部程序访问我们的ContentProvider,
    Enabled属性表示是否启用这个ContentProvider-->
</provider>

MyProvider 最终实现

class MyProvider : ContentProvider() {
    companion object {
        private const val TAG = "MyProvider"
        private const val TABLE_BOOK_DIR = 0
        private const val TABLE_BOOK_ITEM = 1
        private const val TABLE_CATEGORY_DIR = 2
        private const val TABLE_CATEGORY_ITEM = 3
        private const val AUTHORITY = "com.example.androidstudy.contentprovider_study.provider"
    }

    private val uriMatcher by lazy {
        UriMatcher(UriMatcher.NO_MATCH).apply {
            addURI(AUTHORITY, "Book", TABLE_BOOK_DIR)
            addURI(AUTHORITY, "Book/#", TABLE_BOOK_ITEM)
            addURI(AUTHORITY, "Category", TABLE_CATEGORY_DIR)
            addURI(AUTHORITY, "Category/#", TABLE_CATEGORY_ITEM)
        }
    }
    private lateinit var dbHelper: MyDataBaseHelper

    /**
     * 初始化ContentProvider的时候调用
     * 通常会在这里完成对数据库的创建和升级等操作
     * 返回true表示ContentProvider初始化成功,返回false则表示失败
     * @return Boolean
     */
    override fun onCreate(): Boolean {
        return context?.let {
            dbHelper = MyDataBaseHelper(it, "BookStore.db", 2)
            true
        } ?: false
    }

    /**
     * 从ContentProvider中查询数据
     *
     * @param uri 参数用于确定查询哪张表
     * @param projection 参数用于确定查询哪些列
     * @param selection 用于约束查询哪些行
     * @param selectionArgs 用于约束查询哪些行
     * @param sortOrder 参数用于对结果进行排序,
     * @return 查询的结果存放在Cursor对象中返回
     */
    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        val db = dbHelper.readableDatabase
        return when (uriMatcher.match(uri)) {
            TABLE_BOOK_DIR -> {
                // 查询Book的所有数据
                db.query("Book", projection, selection, selectionArgs, null, null, sortOrder)
            }
            TABLE_BOOK_ITEM -> {
                // 查询Book的单条数据
                // 当访问单条数据的时候,调用了Uri对象的getPathSegments()方法
                // 它会将内容URI权限之后的部分以“/”符号进行分割,并把分割后的结果放入一个字符串列表中
                // 那这个列表的第0个位置存放的就是路径,第1个位置存放的就是id了
                val bookId = uri.pathSegments[1]
                for (pathSegment in uri.pathSegments) {
                    Log.d(TAG, "query: pathSegment=$pathSegment")
                }
                db.query("Book", projection, "id = ?", arrayOf(bookId), null, null, sortOrder)
            }
            TABLE_CATEGORY_DIR -> {
                // 查询Category的所有数据
                db.query("Category", projection, selection, selectionArgs, null, null, sortOrder)
            }
            TABLE_CATEGORY_ITEM -> {
                // 查询Category的单条数据
                val categoryId = uri.pathSegments[1]
                for (pathSegment in uri.pathSegments) {
                    Log.d(TAG, "query: pathSegment=$pathSegment")
                }
                db.query(
                    "Category",
                    projection,
                    "id = ?",
                    arrayOf(categoryId),
                    null,
                    null,
                    sortOrder
                )
            }
            else -> null
        }
    }

    /**
     * 根据传入的内容URI返回相应的MIME类型
     * @param uri Uri
     * @return String?
     */
    override fun getType(uri: Uri): String? {
        return when (uriMatcher.match(uri)) {
            TABLE_BOOK_DIR -> {
                "vnd.android.cursor.dir/$AUTHORITY.Book"
            }
            TABLE_BOOK_ITEM -> {
                "vnd.android.cursor.item/$AUTHORITY.Book"
            }
            TABLE_CATEGORY_DIR -> {
                "vnd.android.cursor.dir/$AUTHORITY.Category"
            }
            TABLE_CATEGORY_ITEM -> {
                "vnd.android.cursor.item/$AUTHORITY.Category"
            }
            else -> null
        }
    }

    /**
     * 向ContentProvider中添加一条数据
     *
     * @param uri 用于确定要添加到的表
     * @param values 待添加的数据保存在values参数中
     * @return 返回一个用于表示这条新记录的URI
     */
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        val db = dbHelper.writableDatabase
        return when (uriMatcher.match(uri)) {
            TABLE_BOOK_DIR, TABLE_BOOK_ITEM -> {
                val newBookId = db.insert("Book", null, values)
                Uri.parse("content://$AUTHORITY/Book/$newBookId")
            }
            TABLE_CATEGORY_DIR, TABLE_CATEGORY_ITEM -> {
                val newCategoryId = db.insert("Category", null, values)
                Uri.parse("content://$AUTHORITY/Category/$newCategoryId")
            }
            else -> null
        }
    }

    /**
     * 从ContentProvider中删除数据
     *
     * @param uri 用于确定删除哪一张表中的数据
     * @param selection 用于约束删除哪些行
     * @param selectionArgs 用于约束删除哪些行
     * @return Int 被删除的行数将作为返回值返回
     */
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
        val db = dbHelper.writableDatabase
        return when (uriMatcher.match(uri)) {
            TABLE_BOOK_DIR -> {
                db.delete("Book", selection, selectionArgs)
            }
            TABLE_BOOK_ITEM -> {
                val bookId = uri.pathSegments[1]
                db.delete("Book", "id = ?", arrayOf(bookId))
            }
            TABLE_CATEGORY_DIR -> {
                db.delete("Category", selection, selectionArgs)
            }
            TABLE_CATEGORY_ITEM -> {
                val categoryId = uri.pathSegments[1]
                db.delete("Category", "id = ?", arrayOf(categoryId))
            }
            else -> 0
        }
    }

    /**
     * 更新ContentProvider中已有的数据
     *
     * @param uri 用于确定更新哪一张表中的数据
     * @param values 新数据保存在values参数中
     * @param selection 用于约束更新哪些行
     * @param selectionArgs 用于约束更新哪些行
     * @return Int 受影响的行数将作为返回值返回
     */
    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String>?
    ): Int {
        val db = dbHelper.writableDatabase
        return when (uriMatcher.match(uri)) {
            TABLE_BOOK_DIR -> {
                db.update("Book", values, selection, selectionArgs)
            }
            TABLE_BOOK_ITEM -> {
                val bookId = uri.pathSegments[1]
                db.update("Book", values, "id = ?", arrayOf(bookId))
            }
            TABLE_CATEGORY_DIR -> {
                db.update("Category", values, selection, selectionArgs)
            }
            TABLE_CATEGORY_ITEM -> {
                val categoryId = uri.pathSegments[1]
                db.update("Category", values, "id = ?", arrayOf(categoryId))
            }
            else -> 0
        }
    }
}

在另一个项目中读取共享的数据

class MainActivity : AppCompatActivity() {
    companion object {
        private const val TAG = "MainActivity"
    }

    var bookId: String? = null

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

        add_data.setOnClickListener {
            val uri =
                Uri.parse("content://com.example.androidstudy.contentprovider_study.provider/Book")
            val values =
                contentValuesOf("name" to "Java", "author" to "小明", "pages" to 453, "price" to 43.2)
            val newUri = contentResolver.insert(uri, values)
            bookId = newUri.pathSegments[1]
        }
        update_data.setOnClickListener {
            val uri =
                Uri.parse("content://com.example.androidstudy.contentprovider_study.provider/Book/$bookId")
            val values = ContentValues().apply {
                put("author", "小红")
            }
            contentResolver.update(uri, values, null, null)
        }
        delete_data.setOnClickListener {
            val uri =
                Uri.parse("content://com.example.androidstudy.contentprovider_study.provider/Book/$bookId")
            contentResolver.delete(uri, null, null)
        }
        query_data.setOnClickListener {
            val uri =
                Uri.parse("content://com.example.androidstudy.contentprovider_study.provider/Book")
            val cursor = contentResolver.query(uri,  null, null, null, null)
            while (cursor.moveToNext()) {
                val name = cursor.getString(cursor.getColumnIndex("name"))
                val author = cursor.getString(cursor.getColumnIndex("author"))
                val pages = cursor.getString(cursor.getColumnIndex("pages"))
                val price = cursor.getString(cursor.getColumnIndex("price"))
                Log.d(TAG, "onCreate: name=$name author=$author pages=$pages price=$price ")
            }
            cursor.close()
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容