目录
1.背景
2.问题
3.FileProvider
4.事例
5.原理
文章最后有代码链接
1.背景
targetSdkVersion:25
模拟器:genymotion api7.0
2.问题
Android7.0开始,应用私有目录被限制访问,官方做了如下限制:
1.私有文件的文件权限不应再由所有者放宽,使用MODE_WORLD_READABLE/MODE_WORLD_WRITEABLE将抛出异常
2.向应用外传递file://URI会出发FileUriExposedException
3.FileProvider
当targetSdkVersion>=24时,会存在上述问题,可能涉及到的场景有:拍照,程序安装等。
同时,官方在v4包(api=22开始)中引入FileProvider类用于程序间私有文件的共享。该类继承自ContentProvider,使用时需要在清单文件中注册。
1.使用方法
- 注册
在清单文件中通过标签注册,参考代码对属性进行说明:
属性名 | 意义 | 值 |
---|---|---|
android:name | 组件的路径 | 统一:android.support.v4.content.FileProvider |
android:authorities | 可以理解为标识符,完全自定义 | 推荐以包名+”.fileprovider”方式命名,增加辨别性 |
android:exproted | 该组件是否可以被外部程序访问 | “false”,没必要 |
android:grantUriPermissions | 是否允许为文件设置临时权限 | “true” |
注意:当自定义类继承自FileProvider时,需要更改name属性值为该类的相对路径…
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.mydomain.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
...
</provider>
...
</application>
</manifest>
- 设置可访问的共享文件
FileProvider只能对声明的文件夹下的文件生成uri,该文件夹的声明是在xml中使用标签完成的,下面的例子就是声明私有文件目录下images/下的文件可以临时访问(文件在res/xml/目录下),下面时一个简单的样式:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
...
</paths>
因为的子标签可以有多种,这里对所有进行说明:
标签名 | 作用 |
---|---|
< files-path> | 相当于Context.getFilesDir() |
< cache-path> | 相当于Context.getFilesDir() |
< external-path> | 相当于Environment.getExternalStorageDiretory() |
< external-files-path> | 相当于Context.getExternalCacheDir() |
子标签中属性说明:
属性名 | 意义 | 取值 |
---|---|---|
name | uri路径的分隔符,替换目录的全路径,保证子路径的安全 | 自定义,只体现在uri中,格式为:”my_image“,实际操作无体现,可在后续对比uri中看到该值 |
path | 真正的子目录,代表一个目录 | 不可以指定为一个单一文件,也不可以使用通配符,格式为:”images/” |
实际使用时,一个path的定义格式应该如下:
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
<files-path name="my_docs" path="docs/"/>
</paths>
- 完成配置
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.mydomain.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
说明:
name:为固定值android.support.FILE_PROVIDER_PATHS
resource:所对应的xml文件路径
- 使用
1、通过路径生成要分享的文件File对象
2、使用FileProvider生成uri—FileProvider.getUriForFile()
3、客户端可以使用uri通过ContentResolver.openFileDescriptor获取到一个ParcelFileDescriptor
事例:
authrity为”com.mydomain.fileprovider”的FileProvider,获取到文件”default_image.jpg”文件,改文件位于”images”目录下
File imagePath = new File(Context.getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
note:该代码生成的uri为
content://com.mydomain.fileprovider/my_images/default_image.jpg
- 临时权限的授予方式
1、使用Context.grantUriPermission(package,Uri,mode_flags)方法,使用想要的模式。这个方法通过mode_flags方法授予客户端package的临时权限,有两个取值,FLAG_GRANT_URI_PERMISSION和FLAG_GRANT_WRITE_URI_PERMISSOIN。该方式允许后,可通过revokeUriPermission终止,或者手机重启后
2、通过Intent
- 通过Intent的setData()方法将该uri放入Intent中
- 可以为Intent设置flag,设置一个或两个, FLAG_GRANT_URI_PERMISSION和FLAG_GRANT_WRITE_URI_PERMISSOIN
- 将Intent发送给其他app,大部分情况,通过setResult()来做
这种方法获取的权限,当接收的Activity在栈中一直活跃时都会保留,当activity栈finish时,权限会自动移除。被允许的activity所在的app的其他组件也会被允许该权限。
4.事例
1.场景
将apk放在内部存储空间内,使用Intent进行安装
2.主要代码
- 将apk写入
public class App extends Application {
public static final String TAG = "App";
private static String path;
@Override
public void onCreate() {
super.onCreate();
writeApkToInner("app-debug.apk");
}
public static String getPath() {
return path;
}
private void writeApkToInner(String s) {
File dir = getCacheDir();
if (!dir.exists()) dir.mkdir();
File[] apks = dir.listFiles();
for (File f : apks) {
f.delete();
}
File apk = new File(dir, "test.apk");
path = apk.getAbsolutePath();
InputStream is = null;
OutputStream os = null;
try {
is = getResources().getAssets().open(s);
os = new FileOutputStream(apk);
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) > 0) {
os.write(buffer, 0, len);
os.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (os != null)
os.close();
if (is != null)
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
Log.e(TAG, "write into inner successful");
}
}
- 安装代码
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uriForFile = FileProvider.getUriForFile(this, "com.zzc.sample.fileprovider.fileprovider", new File(App.getPath()));
Log.e(TAG, "uri" + uriForFile.toString());
intent.setDataAndType(uriForFile, "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
- 清单文件
<application
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.zzc.sample.fileprovider.fileprovider"
android:grantUriPermissions="true"
>
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/files_provider">
</meta-data>
</provider>
</application>
- path文件
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<cache-path
name="zzc_files"
path="apks/"/>
</paths>
- uri对比
content://com.zzc.sample.fileprovider.fileprovider/zzc_files/test.apk
file:///data/user/0/com.zzc.sample.fileprovider/cache/apks/test.apk
5.原理
1.FileProvider内部
- PathStrategy
文件和uri之间的对应策略,不依赖动态,保证所有生成的uri能在进程被杀死并在之后的启动中保持一致。 - query方法
该方法会能获取文件名和文件大小的Cursor - openFile方法
该方法的主要作用是将文件的文件描述符FileDescriptor封装为ParcelFileDescriptor作为该方法的返回值,实现Parcelable接口,可在进程间传输
2.自定义处理
该模式依然采用ContentProvider-ContentResolver工作
1.客户端(文件被分享者)主要代码
- 清单文件
<activity android:name=".ResolverActivity">
<intent-filter>
<action android:name="com.zzc.sample.client.SHARE"/>
<data android:mimeType="zzc/client"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
- ResolverActivity
Intent intent = getIntent();
if (intent != null) {
Uri data = intent.getData();
ContentResolver resolver = getContentResolver();
BufferedReader br = null;
try {
ParcelFileDescriptor parcelFileDescriptor = resolver.openFileDescriptor(data, "r");
FileDescriptor fd = parcelFileDescriptor.getFileDescriptor();
br = new BufferedReader(new FileReader(fd));
Log.e(TAG, "content---" + br.readLine());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (br != null) {
br.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2.服务端(文件分享者)主要代码
- 写入Context.getCacheDir()/txts/hello.txt文件
private void writeFile(String s) {
File dir = getCacheDir();
if (!dir.exists()) dir.mkdir();
dir = new File(dir, "txts");
if (!dir.exists()) dir.mkdir();
File file = new File(dir, s);
filePath = file.getAbsolutePath();
if (!file.exists()) {
OutputStreamWriter osw = null;
try {
osw = new OutputStreamWriter(new FileOutputStream(file));
osw.write("hello world");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (osw != null) {
osw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 授予临时权限
Uri data = FileProvider.getUriForFile(this, "com.zzc.sample.fileprovider.fileprovider", new File(App.getFilePath()));
Intent intent = new Intent("com.zzc.sample.client.SHARE");
intent.setDataAndType(data, "zzc/client");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(intent);
3.输出
content---hello world