Android中URI

Android开发:深入理解Uri与内容提供器
本文详细介绍了Android中的Uri,包括Uri的概念、组成、格式和分类。重点讲解了内容提供器,它是Android中实现跨应用数据共享的关键。内容提供器使用ContentResolver进行数据操作,并通过自定义ContentProvider保护数据隐私。此外,还讨论了在实际项目中遇到的关于Uri获取和FileProvider使用的问题。

一、Uri简介

URI(Universal Resource Identifier)通用资源标识符

Uri和URI的区别:Uri是Android开发的,扩展了Java中的一些功能来特定的适用于Android开发,所以,在开发时,只使用Android提供的 Uri 即可。

Uri代表要操作的数据,Android里面的每种可用的资源,包括图像、视频、联系人等都可以用Uri来表示。

二、Uri组成

Uri的组成一般有三部分组成:
访问资源的命名机制、存放资源的主机名、资源自身的名称
例如解释:

https://blog.csdn.net/qq_12345/article/details/7777777

例如,所有联系人的Uri:

content://contacts/people

某张图片的Uri:

content://media/external/images/media/4

以下述Uri为例

content://com.android.providers.media.documents/document/image:1598915
  • uri.getScheme():content
    指的是Uri协议
  • uri.getAuthority():com.android.providers.media.documents
    文件提供器标识
  • uri.getPath():document/image:1598915
    获取文件提供器之后的路径
  • uri.getPathSegments():[document, image:158975]
    获取文件提供器之后的路径,以File.separator切分成数组(自动解码)
  • uri.withAppendedPath(uri, segment):content://com.android.providers.media.documents/image:1598915/segment
    在uri最后添加一个子路径

三、Uri格式

不同类别Uri的格式:

比如
第一行的图片表示,系统自带的文件管理器中的图片的Uri的格式;
第二行的图片表示,第三方集成到系统管理器中的图片的Uri的格式。

序号类别Uri
1图片content://com.android.providers.media.documents/document/image%3A1598915
-图片content://media/external/images/media/1508729
2视频content://com.android.providers.media.documents/document/video%3A1594850
-视频content://media/external/video/media/1594849
3音频content://com.android.providers.media.documents/document/audio%3A920365
4下载content://com.android.providers.downloads.documents/document/raw%3A%2Fstorage%2Femulated%2F0%2FDownload%2Ftest.txt
5手机content://com.android.externalstorage.documents/document/primary%3Atest.txt
6文件管理器file:///storage/emulated/0/test.txt
7文件管理器content://com.jinghong.fileguanlijh.FILE_PROVIDER/storage_root/Android/log.txt
8ES文件管理器content://com.estrongs.files/storage/emulated/0/test.txt
9文件管理器content://com.tencent.mtt.fileprovider/QQBrowser/test.txt

上面的%3A指的是冒号

四、Uri分类

  • 系统的内容提供器
  • 第三方的内容提供器
  • 旧式file类型的uri

系统的内容提供器创建的文件都是来自DocumentsProvider
使用DocumentsContract.isDocumentUri(context, uri)可以判断该uri是否来自系统内容提供器。

五、内容提供器(重点)

  文件存储SP存储以及数据库存储这些持久化技术所保存的数据都只能在当前应用程序中访问,虽然文件和SP存储中提供了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE这两种操作模式,用于供给其它的应用程序访问当前应用的数据,但是这两种模式在Android 4.2中已被废弃,因为Android官方不推荐使用这种方式来实现程序跨程序数据共享的功能,而是应使用内容提供器技术

内容提供器的用法一般有两种:

  • 使用现有的内容提供器来读取和操作相应程序中的数据
  • 创建自己的内容提供器给我们程序的数据提供外部访问接口

5.1 ContentResolver

  对于每一个应用程序来说,如果想要访问内容提供器中共享的数据,就一定要借助ContentResolver类,可以通过Context中的getContentResolver()方法获取到该类的实例。ContentResolver中提供了一系列的方法用于对数据进行CRUD操作。
  不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容Uri。
   内容Uri给内容提供器中的数据建立了唯一标识符,它主要由两部分组成:authority和path

  • authority用于对不同的应用程序做区分,一般为了避免冲突,都会采用程序包名的方式来进行命名。比如某个程序的包名是com.example.app,那么该程序对应的authority就可以命名为com.example.app.provider。
  • path用于对同一应用程序中不同的表做区分,通常都会添加到authority的后面。比如某个程序的数据库里存在两张表:table1和table2,这时就可以将path分别命名为/table1和/table2。
  • 将authority和path进行组合,内容URI就变成了 com.example.app.provider/table1 和 com.example.app.provider/table2,

在字符串的头部加上协议声明,因此内容URI最标准的格式写法即为(以第一个为例):

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

除此之外,我们还可以在这个内容Uri的后面加上一个id:

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

表示调用方期望访问的是com.example.app这个应用的table1表中id为1的数据。
内容Uri的格式主要就只有以上两种: 以路径结尾表示期望访问该表中所有的数据;以id结尾表示期望访问该表中拥有相应id的数据。我们可以使用通配符的方式来分别匹配这两种格式的内容Uri,规则如下:

  • *:表示匹配任意长度的任意字符
  • #:表示匹配任意长度的数字
    所以一个能够匹配任意表的内容URI可以写成:
content://com.example.app.provider/*

一个能够匹配table1表中任意一行数据的内容Uri格式可以写成:

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

内容URI可以非常清楚地表达出我们想要访问哪个程序中哪张表里的数据。

  在得到了内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入,解析的方法很简单,只需调用Uri.parse()方法就可以将内容Uri字符串解析成Uri对象了:

Uri uri = Uri.parse("content://com.example.app.provider/table1");

  这时我们就可以使用这个Uri对象来查询table1表中的数据了:

Cursor cursor = getContentResolver().query(
	uri,
	projection,
	selection,
	selectionArgs,
	sortOrder);

这些参数的意义:

query()方法参数对应SQL部分描述
urifrom table_name指定查询某个应用程序下的某一张表
projectionselect column1, column2指定查询的列名
selectionwhere column = value指定where的约束条件
selectionArgs-为where中的占位符提供具体的值
orderByorder by column1, column2指定查询结果的排序方式

  查询完成后返回的是一个Cursor对象,可以将数据从Cursor对象中逐个读取出来了。读取的思路:通过移动游标的位置来遍历Cursor的所有行,然后再取出每一行中相应列的数据():

if(cursor != null){
	while(cursor.moveToNext()){
		String column1 = cursor.getString(cursor.getColumnIndex("column1"));
		int column2 = cursor.getInt(cursor.getColumnIndex("column2"));
	}
	cursor.close();
}

ContentValues values = new ContentValues();
values.put("column1", "test");
values.put("column2", 1);
getContentResolver().insert(uri, values);

将待添加的数据组装到ContentValues中,然后调用ContentResolver的insert()方法,将Uri和ContentValues作为参数传入即可。
:(把column1的值清空)

ContentValues values = new ContentValues();
values.put("column1", "");
getContentResolver().update(uri, values, "column1 = ? and column2 = ?", new String[]{"test", "1"});

删:(把这条数据删除掉)

getContentResolver().delete(uri, "column2 = ?", new String[]{"1"});

5.2 创建自己的内容提供器

可以通过新建一个类去继承 ContentProvider 的方式来创建一个自己的内容提供器。ContentProvider 类中有 6 个抽象方法,我们在使用子类继承它时,需要将这6个方法全部重写:

public class MyProvider extends ContentProvider{
	/*
	* 通常会在这里完成对数据库的创建和升级等操作
	* 返回true表示内容提供器初始化成功;返回false表示失败
	* 只有当存在ContentResolver尝试访问我们程序中的数据时,内容提供器才会被初始化
	*/
	@Override
	public boolean onCreate(){
		return false;
	}

	/*
	* 从内容提供器中查询数据
	* 使用uri参数来确定查询哪张表
	* projection参数用于确定查询那些列
	* selection和selectionArgs用于约束查询哪些行
	* sortOrder用于对结果进行排序,查询的结果存放在Cursor对象中返回
	*/
	@Override
	public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder){
		return null;
	}

	/*
	*向内容提供器中添加一条数据
	* uri来确定要添加到的表
	* 待添加的数据保存在values参数中,添加完成后,返回一个用于表示这条新纪录的Uri
	*/
	@Override
	public Uri insert(Uri uri, ContentValues values){
		return null;
	}

	/*
	* 更新内容提供器中已有的数据
	* 使用uri来确定更新哪一张表中的数据
	* 新数据保存在values参数中,selection 和 selectionArgs参数用于约束更新哪些行,受影响的行数将作为返回值返回
	*/
	@Override
	public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs){
		return 0;
	}

	/*
	* 从内容提供器中删除数据,使用uri参数来确定删除哪一张表中的数据
	* selection 和 selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回
	*/
	@Override
	public int delete(Uri uri, String selection, String[] selectionArgs){
		return 0;
	}

	/*
	* 根据传入的内容Uri来返回相应的MIME类型
	*/
	@Override
	public String getType(Uri uri){
		return null;
	}
}

任何一个应用程序都可以使用ContentResolver来访问我们程序中的数据。但是如何才能保证隐私数据不会泄露出去呢? 因为所有的CRUD操作都一定要匹配到相应的内容Uri格式才能进行,而我们不可能像UriMatcher中添加隐私数据的URI,所以这部分数据根本无法被外部程序访问到,安全问题也就不存在了。(不太理解)

5.3 内容提供器的一些属性

  在Manifest中注册provider时,假设我们的内容提供器命名为 DatabaseProvider,所以android:name属性指定了DatabaseProvider的类名,authority指定为com.example.databasetest.provider,其中Exported属性表示是否允许外部程序访问我们的内容提供器,Enabled属性表示是否启动这个内容提供器。

关于内容提供器的具体实例,可以参考《第一行代码》7.2.4节。

但是android:name可以指定为

<provider>
 android:name="android.support.v4.content.FileProvider"
 ··· ···
 </provider>

表示:使用默认的v4的FileProvider

具体项目中遇到的一些问题

我想要访问文件管理器中某条路径下的照片,路径类似如下:

/storage/emulated/0/Android/data/com.example.test/files/toolkit/zxy.jpg

最开始的思路是通过选择文件,看一下这个路径对应的Uri是什么,然后通过调用文件选择器(调用如下),在onActivityResult中查看到的Uri是下面这样的:

通过文件选择器,选择文件,获得uri,分享的项目:
https://pan.baidu.com/s/1d6Fp7goe0td0KRFiGboF8A
i6qt

Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
intent.addCategory(Intent.CATEGORY_OPENABLE);
try {
    startActivityForResult(Intent.createChooser(intent, getString(R.string.select_file)), FILE_SELECT_CODE);
    overridePendingTransition(0, 0);
} catch (Exception ex) {
     // Potentially direct the user to the Market with OnProgressChangeListener Dialog
    Toast.makeText(this, getString(R.string.please_install_filemanager), Toast.LENGTH_SHORT).show();
}
  @Override
    protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == FILE_SELECT_CODE && resultCode == RESULT_OK) {
            shareFileUrl = data.getData(); 
            tvShareFileUri.setText(shareFileUrl.toString());
        } else if (requestCode == REQUEST_SHARE_FILE_CODE){
            // todo share complete.
        }
    }
content://com.coloros.filemanager.fileprovider/root/storage/emulated/0/Android/data/com.example.test/files/toolkit/zxy.jpg

所以想着能不能获取文件管理器的包名,然后自己拼接一下,得到一个Uri,但是首先第一个问题是:

我拿不到文件管理器的包名

第二个问题是:

就算拿到包名,人家不一定按照我想要拼接的规则命名auth,我同样是取不到Uri。

然后经过大神指导,可以在自己的应用中注册一个fileprovider,然后调用getUriForFile方法拿到uri(下面写的是root-path指的是共享的文件夹为Android设备的根目录)。
xml文件可以path全部,例如:

<?xml version="1.0" encoding="utf-8"?>
<paths>
	<root-path
		name="all"
		path="."
	/>
</paths>

在需要获取Uri的地方:

FileProvider.getUriForFile(this, "com.example.test.fileprovider", new File("/storage/emulated/0/Android/data/com.example.test/files/toolkit/zxy.jpg"));

得到的Uri为:

content://com.example.test.fileprovider/all/storage/emulated/0/Android/data/com.example.test/files/toolkit/zxy.jpg

可是这是为什么呢?
倒着来分析:

1、FileProvider.getUriForFile的第二个参数是在Manifest文件中注册的auth值,第三个参数为要共享的文件,这个文件一定位于我们在path文件中添加的子目录里。
2、path文件
在res/xml目录下新建的一个xml文件,用于存放应用需要共享的目录文件。添加完共享目录后,再在元素中使用元素将res/xml中的path文件与注册的FileProvider链接起来。

包含的子元素可以看关于Android7.0适配中FileProvider部分的总结

  • < root-path >指的是Android设备根目录下的文件
  • < files-path >指的是内部存储空间应用私有目录下的files/目录,等同于 Context.getFIlesDir() 所获取的目录路径。
  • path 属性用于指定当前子元素所代表的目录下需要共享的子目录的名称,注意的是,path属性值不能使用具体的独立文件名,只能是目录名。
  • name 属性用于给path属性所指定的子目录名称取一个别名。后续生成 content://Uri时,会使用这个别名代替真实的目录名,这样做是为了提高安全性。

即,相当于自己的这个应用将Android根目录设为了共享目录

疑问: 这样不会造成其他应用的隐私泄露吗?
在我的应用中甚至可以分享微信下面的内容

Intent intent = new Intent(Intent.ACTION_SEND);
File newFile = new File("/storage/emulated/0/Android/data/com.tencent.mm/files/tbslog/IMG20210113085644.jpg");
Uri uri = FileProvider.getUriForFile(AlbumDetailsActivity.this, "com.vbooster.vbooster_privace_z_space.fileprovider", newFile);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setType("*/*");
startActivity(Intent.createChooser(intent, "分享至:"));

但是

我获取到的以 “content://” 开头的Uri无法将多张图片分享出去,但是却可以使用"file://"开头的Uri将

 uri = Uri.fromFile(new File(truePath));

的方式,将多张图片分享出去,获取到的Uri为:

file:///storage/emulated/0/Android/data/com.vbooster.vbooster_privace_z_space/files/toolkit/private_album/zfenshe
n_img.jpg

分享时,需要用到Uri来找到资源:

Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, imageUri); //以file开头的URI可支持多图分享
shareIntent.setType("image/*");
startActivity(Intent.createChooser(shareIntent, "分享到"));

还需要添加:
但是如果使用 “file://” 开头的Uri会报错:

W/System.err: android.os.FileUriExposedException:

原因在于:从Android7.0开始,不再允许在app中把file://Uri暴露给其他app。Google认为使用file://Uri存在一定的风险。

但是如果硬要使用"file://"的话,只能在application的onCreate()里加上:

if(Build.VERSION.SDK_INT >= 18){
     StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
     StrictMode.setVmPolicy(builder.build());
     builder.detectFileUriExposure();
}

参考文章:
1、URI和URL的区别:https://www.cnblogs.com/anke-z/p/13084107.html
2、利用 Android 系统原生 API 实现分享功能:https://juejin.cn/post/6844903602851430413#heading-0
3、关于 Android 7.0 适配中 FileProvider 部分的总结:https://blog.csdn.net/growing_tree/article/details/71190741
4、《第一行代码》
5、解决异常android.os.FileUriExposedException:https://blog.csdn.net/piaomiaozaitianya/article/details/79657397

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值