ContentProvider学习(四)--Uri相关

1,950 阅读7分钟

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

概述

经过前三篇文章的学习,我们明白了如何通过ContentProvider操作其它应用的数据,同时了解了如何通过ContentProvider向其它应用提供数据,之后还学习了如何监控数据的变化以及通过LoaderManager在工作线程中加载数据。

任何操作我们都需要通过Uri去执行,这篇笔记主要是学习和URI相关的类和方法。

Uri

我们可以通过Uri中提供的部分方法来构造一个Uri

parse(String uriString)

我们可以通过parse方法来构建一个Uri,如下所示:


    Uri.parse("content://com.project.mystudyproject.provider/Student")
    

通过上面的方法我们就可以创建出一个Uri,一般情况下我们在使用ContentProvider的时候只需要提供schemeauthoritypath就可以了,上面的方法就只提供了这三个字段。

withAppendedPath(Uri baseUri, String pathSegment)

我们可以通过这个方法将路径拼接到已有的Uri中。例如我们的Provider不仅会提供针对Student表的数据,还会提供针对Teacher表的数据,但是由于是同一个ContentProvider,所以schemeauthority都是一样的,此时我们就可以使用这个方法创建针对两个不同表的Uri

        //scheme
        val scheme = "content://"
        //authority
        val authority = "com.project.mystudyproject.provider"
        //baseUri
        val baseUri = Uri.parse("$scheme$authority")
        //学生表
        val studentPath = "Student"
        //教师表
        val teacherPath = "Teacher"
        //学生Uri
        val studentUri = Uri.withAppendedPath(baseUri,studentPath) //content://com.project.mystudyproject.provider/Student
        //教师Uri
        val teacherUri = Uri.withAppendedPath(baseUri,teacherPath) //content://com.project.mystudyproject.provider/Teacher

encode(String s,[String allow])decode(String s)

由于Uri中的字符必须是ASCII码中允许的字符,所以针对部分字符需要进行编码,上面的这两个方法就是用于编码字符和反编码字符。encode方法中第二个参数表示不需要参与编码的字符串,如下所示:


        val encode = Uri.encode("我是谁")//%E6%88%91%E6%98%AF%E8%B0%81
        val encode1 = Uri.encode("我是谁","我")//我%E6%98%AF%E8%B0%81
        val decode = Uri.decode("%E6%88%91%E6%98%AF%E8%B0%81")//我是谁
        val decode1 = Uri.decode("我%E6%98%AF%E8%B0%81")//我是谁

fromFile(File file)

通过这个方法我们可将某一个文件/文件夹转换为对应Uri,如下所示:


        val file = this.externalCacheDir // /storage/emulated/0/Android/data/com.project.mystudyproject/cache
        val fileUri = Uri.fromFile(file) // file:///storage/emulated/0/Android/data/com.project.mystudyproject/cache

fromParts(String scheme, String ssp,String fragment)

这个方法可以让我们定义Uri的各个部分,注意这里最后一个参数是fragmentpath是包含在ssp中的。如下所示:


val uri = Uri.fromParts("content","//com.project.mystudyproject.provider/Student",null) //content:%2F%2Fcom.project.mystudyproject.provider%2FStudent

需要注意的是:这个方法创建的是一个不透明的Uri,也就是从模式符号:和Fragment符号#之间的字符将会全部被编码,所以看起来不是我们熟悉的content://ssp#fargment的形式。

Uri.Builder

如果上面看过withAppendedPath这个方法的源码,可以发现这个方法内部就是通过Uri.Builder类中的相关方法将path拼接进去的,通过这个类中的方法也能够创建我们需要的Uri

通过Uri.Builder()构造Uri

下面的代码通过创建Uri.Builder()对象,并调用其中的方法创建了一个简单的Uri,如下所示:

        //Uri.Builder
        val builder = Uri.Builder()
        val builderUri = builder.scheme("content")
            .authority("com.project.mystudyproject.provider")
            .path("Student")
            .build() //content://com.project.mystudyproject.provider/Student

path(String path)encodePath(String newSegment)

这两个方法用于设置Uripath,从方法名称上也可以看出,一个是编码后的字符,一个是没有编码的字符,如下所示:

        val builder = Uri.Builder()
        val builderUri = builder
            .scheme("content")
            .authority("com.project.mystudyproject.provider")
            .path("我是谁")                                          //content://com.project.mystudyproject.provider/%E6%88%91%E6%98%AF%E8%B0%81
            .encodedPath("我是谁")                                   //content://com.project.mystudyproject.provider/我是谁
            .encodedPath(Uri.encode("我是谁"))                       //content://com.project.mystudyproject.provider/%E6%88%91%E6%98%AF%E8%B0%81
            .build()

从上面的结果可以看出,path()方法会将其参数中的特殊字符进行编码,而encodedPath()则直接使用其参数的值。

除此以外,authority()encodedAuthority(),fragment()encodedFragment()query()encodeQuery()均是这样,不做演示。

opaquePart(String s)encodedOpaquePart(String s)

这两个方法用于设置一个不透明的Urissp部分,如下所示:

        val builder = Uri.Builder()
        val builderUri = builder
            .scheme("content")
            .appendPath("我是谁")
            .appendQueryParameter("name","张三")
            .opaquePart("//com.project.mystudyproject.provider")
            .fragment("fragment")
            .encodedFragment("encodeFragment")
            .build()     //content:%2F%2Fcom.project.mystudyproject.provider#encodeFragment

我们需要明确的是:pathqueryParams部分都是属于Uri中的ssp部分,而对于不透明Uri来说,由于无法分层,所以我们只能一次指定属性,后面不能再向其中添加属性,这也是上面的appendPath()appendQueryParamter()没有添加到最终的Uri中的原因。

appendPath(String path)appendEncodedPath(String newSegment)

这两个方法用于将路径拼接到Uri中,从方法名可以看出,appendPath()可以接收任何字符串,拼接的时候会对,字符串进行编码操作。而appendEncodedPath()可以接收编码后的字符串,但是这个方法并不会校验字符串是否被编码,所以如果我们有特殊字符(包括汉字等)不想被编码直接使用,可以调用这个方法。如下所示:

        val builder = Uri.Builder()
        val builderUri = builder.scheme("content")
            .authority("com.project.mystudyproject.provider")
            //.appendEncodedPath(Uri.encode("我是谁"))  //content://com.project.mystudyproject.provider/%E6%88%91%E6%98%AF%E8%B0%81
            //.appendEncodedPath("我是谁")              //content://com.project.mystudyproject.provider/我是谁
            //.appendPath("我是谁")                     //content://com.project.mystudyproject.provider/%E6%88%91%E6%98%AF%E8%B0%81
            .build()

appendQueryParameter(String key, String value)

这个方法用于将查询参数添加到Uri中,如下所示:

        val builder = Uri.Builder()
        val builderUri = builder
            .scheme("content")
            .authority("com.project.mystudyproject.provider")
            .appendPath("我是谁")
            .appendQueryParameter("name","张三")
            .build()                                              //content://com.project.mystudyproject.provider/%E6%88%91%E6%98%AF%E8%B0%81?name=%E5%BC%A0%E4%B8%89

可以看到,查询参数已经被加入带了Uri的末尾。

Uri转换为Uri.Builder

通过调用Uri对象uri.buildUpon()方法,我们可以获得一个Uri.Builder对象,使用这个对象我们就可以执行上面提到的各种操作:


builderUri.buildUpon().appendPath("test")

ContentUris

ContentUris中的方法主要是用于对Uri中的id进行操作。本身Uri中不存在id属性,使用ContentUris中的方法可以将id添加到Uripath的末尾,这样可以很方便我们对指定的数据进行监听等。

withAppendedId(Uri,Long)

这个方法用于将id拼接到指定的Uri路径后面,如下所示:

val idUri = ContentUris.withAppendedId(builderUri,10L)

//builder uri is:content://com.project.mystudyproject.provider/Student?name=%E5%BC%A0%E4%B8%89
//idUri is: content://com.project.mystudyproject.provider/Student/10?name=%E5%BC%A0%E4%B8%89

可以看到,id为10的参数拼接到了路径的后面。

appendId(Uri.Builder,Long id)

这个方法也是将id拼接到指定Uri的路径后面,不同之处在于接收的是一个Uri.Builder,返回的也是一个Uri.Builder,如下所示:

val builder = builder
            .scheme("content")
            .authority("com.project.mystudyproject.provider")
            .appendPath("Student")
            .appendQueryParameter("name","张三")

val idUri = ContentUris.appendId(builder,10L).build()  //content://com.project.mystudyproject.provider/Student/10?name=%E5%BC%A0%E4%B8%89

执行结果和上面的执行结果是一样的。

parseId(Uri)

这个方法用于从Uri中获取id,如下所示:

        ContentUris.parseId(idUri)     //10
        ContentUris.parseId(studentUri)//catch Exception: java.lang.NumberFormatException: For input string: "Student"

可以看出,对于有idUri可以正常获取到id,对于没有idUri则会抛出异常。

removeId(Uri)

这个方法在Android 29及以上版本提供,由于移除Uri中的id.

        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            val removeResult = ContentUris.removeId(idUri)
            Log.i(TAG, "initUI: removeIdResultUri is:$removeResult") //content://com.project.mystudyproject.provider/Student?name=%E5%BC%A0%E4%B8%89
        }

UriMatcher

这个类可以帮助我们对Uri进行比较。当我们的ContentProvider可以提供多个Uri的时候使用这个类非常好用。我们可以使用add()方法将我们的Uri加入进去,然后提供一个id,这样当接收到和Uri相关的操作的时候,我们就可以使用id去进行比较,如下所示:

首先定义我们可以提供的Uri的相关信息:

    //scheme
    const val CONTENT_PROVIDER_SCHEME = "content://"

    //authority
    const val STUDENT_CONTENT_PROVIDER_AUTHORITY =
        "com.project.mystudyproject.provider.StudentProvider"

    //学生表的路径
    const val STUDENT_CONTENT_PROVIDER_STUDENT_PATH = "/$TABLE_STUDENT"

针对这两个Uri我们再来定义其id

    //学生表Uri id
    const val STUDENT_URI_ID = 10
    //单个学生的Uri id
    const val STUDENT_ITEM_URI_ID = 11

接着我们创建UriMatcher并将这两个Uri加入进去

    private val mUriMatcher by lazy {
        UriMatcher(UriMatcher.NO_MATCH).apply {
            this.addURI(
                StudentProviderConstant.STUDENT_CONTENT_PROVIDER_AUTHORITY,
                StudentProviderConstant.STUDENT_CONTENT_PROVIDER_STUDENT_PATH,
                StudentProviderConstant.STUDENT_URI_ID
            )
            this.addURI(
                StudentProviderConstant.STUDENT_CONTENT_PROVIDER_AUTHORITY,
                "${StudentProviderConstant.STUDENT_CONTENT_PROVIDER_STUDENT_PATH}/#",
                StudentProviderConstant.STUDENT_ITEM_URI_ID
            )
        }
    }

现在我们就可以使用match(Uri)方法进行比较了,如下所示演示了在query()方法中查询不同的Uri对应的数据:

    override fun query(
        uri: Uri,
        projection: Array<out String>?,
        selection: String?,
        selectionArgs: Array<out String>?,
        sortOrder: String?
    ): Cursor? {
        Log.i("zyf", "query: uri is:$uri")
        //首先对Uri进行比较
        when(mUriMatcher.match(uri)){
            StudentProviderConstant.STUDENT_URI_ID -> {
                //需要获取学生数据表中的数据
                return StudentDBDao.query(projection, selection, selectionArgs, sortOrder)
            }
            StudentProviderConstant.STUDENT_ITEM_URI_ID -> {
                //需要获取学生表中指定id的数据
                var select = selection ?: String()
                select += if(selection == null )
                    " $TABLE_STUDENT_ID = ? "
                    else
                    " AND $TABLE_STUDENT_ID = ? "
                //获取id
                val id = ContentUris.parseId(uri)
                var list = mutableListOf<String>()
                selectionArgs?.let {
                    list.addAll(it.toList())
                }
                list.add(id.toString())
                return StudentDBDao.query(projection, select, list.toTypedArray(), sortOrder)
            }
        }
        return null
    }

如上所示,我这边其实就是在获取到id之后直接将id加入到参数中,当然这个功能在查询方直接就可以设置进去,但是有时候我们并不知道对方表的id的列名,就无法在查询的时候将id直接设置进去。

另外,上面都是在同一个表中进行的操作,如果我们的ContentProvider可以对外提供多张表的数据的时候,UriMatcher可以简化我们判断的操作,使得我们的判断更加准确。

本篇学习笔记的代码位置:点击查看代码