什么是内容提供器

内容提供器(Content Provider)是一个Android系统提供的一个对自身存储的内容进行管理或者对其他应用存储的内容进行访问的功能,即提供内容在不同应用之间共享的功能。

基本协议格式

对于简单传递数据,只需要知道基本的格式就可以,格式:content://org.z5r.ta.provider/test

其中org.z5r.ta.provider是你的提供器的路径,test是提供器内容存储的表的名称。如果你想指定一个库,可以写成content://org.z5r.ta.provider/test/数据库名称

参考:https://developer.android.com/guide/topics/providers/content-provider-creating?hl=zh-cn#content-uri-patterns

如何实现一个Content Provider

以下所有代码均由 Kotlin 编写

  1. 我们在应用中新建一个名为TestContentProvider的类并继承Android提供的原有的ContentProvider,随后会提示我们需要实现相应的方法。代码如下:

    package org.z5r.ta.provider
    
    import android.content.ContentProvider
    import android.content.ContentValues
    import android.database.Cursor
    import android.net.Uri
    class TestContentProvider: ContentProvider() {
    
        override fun onCreate(): Boolean {
            TODO("Not yet implemented")
        }
    
        override fun query(
            uri: Uri,
            projection: Array<out String>?,
            selection: String?,
            selectionArgs: Array<out String>?,
            sortOrder: String?
        ): Cursor? {
            TODO("Not yet implemented")
        }
    
        override fun getType(uri: Uri): String? {
            TODO("Not yet implemented")
        }
    
        override fun insert(uri: Uri, values: ContentValues?): Uri? {
            TODO("Not yet implemented")
        }
    
        override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
            TODO("Not yet implemented")
        }
    
        override fun update(
            uri: Uri,
            values: ContentValues?,
            selection: String?,
            selectionArgs: Array<out String>?
        ): Int {
            TODO("Not yet implemented")
        }
    }
  2. 初始化一些要用到的变量和内部辅助类

    class TestContentProvider: ContentProvider() {
    
        companion object {
            // 用于打印日志的TAG
            const val TAG: String = "TestContentProvider"
            // 数据库名称
            const val DATABASE_NAME: String = "test.db"
            // 数据库版本号(在这里并没什么用)
            const val DATABASE_VERSION: Int = 1
            // 表名称
            const val TEST_TABLE_NAME: String = "test"
            // 内容提供器的路径
            private const val AUTHORITY: String = "org.z5r.ta.provider"
            // 路径匹配,暂时为空,后面用来判断内容路径是否匹配
            var sUriMatcher: UriMatcher? = null
            // Uri匹配结果编码映射(用于区分操作)
            const val TEST: Int = 1
            // 同上
            const val TEST_ID: Int = 2
            // 查询的字段投影,将调用者传入查询的列名映射到数据库字段名
            var testProjectionMap: HashMap<String, String>? = null
            // 辅助操作数据库
            lateinit var dbHelper: DatabaseHelper
            // 构造提供器Uri
            val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/test")
            // 内容类型,虽然写什么都行,但还是按照标准来写:vnd.android.cursor.dir/这里自定义
            const val CONTENT_TYPE = "vnd.android.cursor.dir/vnd.z5r.test"
            // 第一个字段,ID(看情况写,这里就写一个键值对的表)
            const val ID = "_id"
            // 第二个字段,result
            const val RESULT = "result"
        }
    
        /**
         * 内部类,数据库版本管理辅助类
         */
        class DatabaseHelper internal constructor(context: Context?) :
            SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
            override fun onCreate(db: SQLiteDatabase) {
                // 建表
                db.execSQL(((((("CREATE TABLE $TEST_TABLE_NAME").toString() + " (" + ID)
                        + " LONG PRIMARY KEY," + RESULT) + " VARCHAR(255));")))
            }
    
            override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
                Log.w(TAG, "Upgrading database from version $oldVersion to $newVersion, which will destroy all old data")
                // 删表
                db.execSQL("DROP TABLE IF EXISTS $TEST_TABLE_NAME")
                onCreate(db)
            }
        }
    
        // 其他代码 ...
    }
  3. 实现onCreate方法

        override fun onCreate(): Boolean {
            // 初始化数据库辅助对象
            dbHelper = DatabaseHelper(context)
            // 初始化uri匹配结果为不匹配
            sUriMatcher = UriMatcher(UriMatcher.NO_MATCH)
            // 添加要匹配的Uri
            // AUTHORITY:提供器路径
            // TEST_TABLE_NAME:表名
            // TEST:Uri匹配结果编码映射
            sUriMatcher?.addURI(AUTHORITY, TEST_TABLE_NAME, TEST)
            sUriMatcher?.addURI(AUTHORITY, "$TEST_TABLE_NAME/#", TEST_ID)
            // 初始化数据库字段投影
            testProjectionMap = HashMap<String, String>()
            testProjectionMap!![ID] = ID
            testProjectionMap!![RESULT] = RESULT
            // 返回True,要做什么异常处理就自己写吧
            return true
        }
  4. 实现query查询方法

        override fun query(
            uri: Uri,
            projection: Array<out String>?,
            selection: String?,
            selectionArgs: Array<out String>?,
            sortOrder: String?
        ): Cursor? {
            // 拷贝条件字段
            var newSelection = selection
            // 构建数据库操作对象
            val qb = SQLiteQueryBuilder()
            // 指定表名
            qb.tables = TEST_TABLE_NAME
            // 指定字段投影
            qb.projectionMap = testProjectionMap
            // 根据Uri匹配结果编码映射选择对应的操作
            when (sUriMatcher?.match(uri)) {
                TEST -> {}
                TEST_ID -> {
                    // 根据ID过滤数据
                    newSelection = selection + "_id = " + uri.lastPathSegment
                }
                // 不支持的操作
                else -> throw IllegalArgumentException("Unknown URI $uri")
            }
            // 拷贝数据库辅助类的读取对象
            val db = dbHelper.readableDatabase
            // 开始查询
            val c = qb.query(db, projection, newSelection, selectionArgs, null, null, sortOrder)
            // 注册内容变化监听
            c.setNotificationUri(context?.contentResolver, uri)
            // 返回结果
            return c
    }
  5. 实现getType方法

        override fun getType(uri: Uri): String {
            // 匹配Uri
            when (sUriMatcher!!.match(uri)) {
                // 如果匹配1操作就返回内容类型
                TEST -> return CONTENT_TYPE
                else -> throw java.lang.IllegalArgumentException("Unknown URI $uri")
            }
        }
  6. 实现insert数据插入

        override fun insert(uri: Uri, values: ContentValues?): Uri {
            // 判断Uri是否匹配1操作
            if (sUriMatcher!!.match(uri) != TEST) {
                throw java.lang.IllegalArgumentException("Unknown URI $uri")
            }
            // 获取要插入的值,如果为空就返回一个空的Map
            val newVal: ContentValues = if (values != null) {
                ContentValues(values)
            } else {
                ContentValues()
            }
            // 获取数据库辅助操作的写库对象
            val db = dbHelper.writableDatabase
            // 开始插入数据
            // 表名,字段,值
            val rowId = db.insert(TEST_TABLE_NAME, RESULT, newVal)
            // 判断ID是否大于0(大于0表示成功)
            if (rowId > 0) {
                // 获取新的数据行的uri
                val testUri = ContentUris.withAppendedId(CONTENT_URI, rowId)
                // 通知内容获取器某行的内容发生了变化
                context!!.contentResolver.notifyChange(testUri, null)
                // 返回uri
                return testUri
            }
            // 没插入成功
            throw SQLException("Failed to insert row into $uri")
        }
  7. 实现delete数据删除方法

        override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
            // 获取数据库辅助操作的写库对象
            val db = dbHelper.writableDatabase
            // 拷贝条件字段
            var newSelection = selection
            // 匹配Uri操作
            when (sUriMatcher?.match(uri)) {
                TEST -> {}
                TEST_ID -> {
                    // 根据ID过滤数据
                    newSelection = selection + "_id = " + uri.lastPathSegment
                }
                else -> throw IllegalArgumentException("Unknown URI $uri")
            }
            // 执行删除
            val count = db.delete(TEST_TABLE_NAME, newSelection, selectionArgs)
            // 通知内容获取器
            context?.contentResolver?.notifyChange(uri, null)
            // 返回删除的行数
            return count
        }
  8. 实现update数据更新

        override fun update(
            uri: Uri,
            values: ContentValues?,
            selection: String?,
            selectionArgs: Array<out String>?
        ): Int {
            // 获取数据库辅助操作的写库对象
            val db = dbHelper.writableDatabase
            // 更新的数量
            val count: Int
            // 匹配Uri操作
            when (sUriMatcher!!.match(uri)) {
                // 根据条件更新数据
                TEST -> count = db.update(TEST_TABLE_NAME, values, selection, selectionArgs)
                else -> throw java.lang.IllegalArgumentException("Unknown URI $uri")
            }
            // 通知内容更新
            context!!.contentResolver.notifyChange(uri, null)
            // 返回影响行数
            return count
        }

注册并定义权限

内容提供器不是写了就可以用,还需要注册和配置权限。

    <!-- AndroidManifest.xml -->
    <application android:name="application的内容保持原有不变">
        <provider
            android:name=".provider.TestContentProvider(这里是提供器的包路径)"
            android:authorities="org.z5r.ta.provider(这里是提供器路径,和上面Kotlin代码部分定义的AUTHORITY保持一致)"
            android:permission="android.permission.READ_USER_DICTIONARY(权限,就写这个就行)"
            android:exported="true(是否公开,如果不想让其他应用访问就需要改为false)" >
                <grant-uri-permission android:path="test(这是授权的路径“/”后面的部分)" />
        </provider>
    </application>

更多权限需要参考这里:https://developer.android.com/privacy-and-security/security-tips?hl=zh-cn#ContentProviders

使用内容提供器

  1. 申请权限

    <!-- AndroidManifest.xml -->
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
        <!-- 用于获取数据,不写会拒绝访问 -->
        <uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
        <!-- 用于获取数据,不写会什么都访问不到(找不到提供器 Failed to find provider info for xxxx) -->
        <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" />
        <!-- 其他代码 -->
    </manifest>
  2. 使用提供器

            val uri = Uri.parse("content://org.z5r.ta.provider/test")
            val resObj = contentResolver.query(uri, null, null, null)
            // 这里是必须写的,不写的话会抛出异常:android.database.CursorIndexOutOfBoundsException: Index -1 requested, with a size of 1
            // 需要指定初始指针位置
            resObj?.moveToFirst()
            val colIdx = resObj?.getColumnIndex("result")
            val res = colIdx?.let { resObj.getStringOrNull(it) }
            if (res != null) {
                // 这里可以直接处理获取到的数据
            }

摸🐟从未停止,努力从未开始。