导论
我们都知道ContentProvider用于App间的数据共享,那么如果App间想要共享文件该怎么办呢?别担心,Google为我们提供了一个类,是ContentProvider的子类——FileProvider用于在不同应用间共享文件。
既然是ContentProvider的子类,那么整个的共享流程其实也就差不了太多,共享文件的整个思路是:当一个app想向一个共享了文件的app发送请求获取文件时,大多数情况下,这个请求都会开启一个分享的文件列表的Activity(该Acitivity隶属于分享文件app,目的是为了让用户选择想要获取的文件,因为它可能分享了不止一个文件)。用户选择完文件之后,分享文件的app(以下称为Server app,请求文件app称为client app)返回文件的URI和权限给client app。client app就可以根据拿到的URI去找到文件,有了权限也能对对应文件进行读写操作了。
当然你可能会有疑惑为什么是返回URI而不是直接把文件传输给client app或者直接将文件地址给它呢?
这就涉及到了一些安全和一些传输的问题,如果我们直接把文件内容或者整个文件传输给client app,那么在这个文件非常大的时候,十分的消耗资源。但是如果我们直接把文件地址传给其他app,那么就相当于把自身app的文件共享地址暴露了出来,这是非常危险的,因为我们无法预估其他app拿到这个地址之后会做什么。如果我们在这个地址下也有其他的共享文件,我们并不能保证他们的安全。因此我们让app共享文件以一种提供content URI的形式提供一个对文件的安全的操作:在Android中FileProvider组件可以通过getUriForFile方法为File生成一个Contet URI。具体过程下面会提到。
先是Server App端的实现
指定FileProvider
为App定义一个FileProvider,和ContentProvider一样需要在manifest文件中登记。在manifest中指定authority属性用来生成content URI,这些都和ContentProvider一样,不同的在于我们还要额外指定一个分享xml文件的目录。即我们所分享的xml文件都应该在该目录之下。
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.zln.admin.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths"/>
provider>
其中
resourece属性指向res/xml/filepaths文件,在这个文件中filepaths文件中存放的是我们想要分享的目录。
指明可分享的目录
一旦在manifest文件中添加了FileProvider,那么就必须指明想要分享的文件目录。要指明这个目录,首先在res/xml文件夹中创建一个filepaths.xml文件.在这个文件中通过添加xml元素来指明目录。
<paths>
<files-path path="text/" name="text"/>
paths>
在这里,
tag共享了app私有内存files目录下的一个目录。Path属性则说明分享的是files/下的text目录,也就是说我们共享的文件是在files/text/这个目录之下的。Name属性则是告诉FileProvider添加path段到Content URI中,这样就达到了隐藏地址的效果。这样说可能还是比较抽象,举个例子吧:
通过FileProvider的getUriForFile形成的URI的格式是: content://authorities/name/文件名,例如我们在files/text目录下有一个test.txt的文件需要共享,我们通过FileProvider形成传给其他app的URI形式就是:content://com.zln.admin.fileprovider/text/test.txt
元素有许多的子元素,每一个子元素都分别指明了不同的分享目录。除了
还是
元素分享外部内存中的目录,
分享内部缓存中的目录。
注意:在xml文件指定是唯一指定分享目录的方法,不能在代码中设置和添加分享目录,并且只有在目录下的文件才能共享。
到此为止,FIleProvider的登记已经完成了,总结一下在manifest文件中登记FileProvider需要的几步:
1、先在Manifest中注册
2、通过的resources属性中指向filepaths文件
3、在res/xml/filepaths文件中,指定共享目录
public class MainActivity extends Activity {
Button addFile;
Button check;
String filename = "test.txt";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
addFile = (Button)findViewById(R.id.btn);
check = (Button)findViewById(R.id.check);
//创建文件夹与文件
addFile.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
File file = new File(getFilesDir(),"text");
if(!(file.exists())){
file.mkdirs();
}
File fileName = new File(file,"test.txt");
if(!(fileName.exists())){
try {
fileName.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
try {
FileOutputStream fileOutputStream = new FileOutputStream(getFilesDir()+"/text/test.txt",true);
PrintStream ps = new PrintStream(fileOutputStream);
ps.println("testContent");
ps.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
});
//检查数据是否写入
check.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
try {
File file = new File(getFilesDir()+"/text",filename);
FileInputStream fileInputStream = new FileInputStream(file);
Log.e("MainActivity",file.getAbsolutePath());
byte[] buff = new byte[1024];
int hasRead = 0;
while((hasRead = fileInputStream.read(buff))>0){
Toast.makeText(MainActivity.this,new String(buff,0,hasRead),Toast.LENGTH_SHORT).show();
}
fileInputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
注意:在这里笔者被坑过,之前在该Activity读写文件时,我使用的是openFileInput(String filename) 方法来读取私有内存下的文件,该方法的参数是文件名,目录是在files目录下,并不是这个Demo中我们设定的files/text目录下的文件。因此读写的时候我们应该用FileInputStream/FileOutputStream指定路径进行读写。
<activity android:name=".ResultActivity">
<intent-filter>
<action android:name="android.intent.action.PICK"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.OPENABLE"/>
<data android:mimeType="text/plain"/>
intent-filter>
activity>
在代码中实现回应:
public class ResultActivity extends Activity {
ListView listView;
//app内存根目录
private File mPrivateRootDir;
//text目录
private File mTextDir;
File[] mTextFiles;
String[] mTextFilenames;
Intent mResultIntent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_result);
listView = (ListView)findViewById(R.id.listView);
mResultIntent = new Intent("com.zln.admin.ACTION_RETURN_FILE");
mPrivateRootDir = getFilesDir();
mTextDir = new File(mPrivateRootDir,"text");
mTextFiles = mTextDir.listFiles();
mTextFilenames = new String[mTextFiles.length];
for(int i = 0;inull);
listView.setAdapter(new ArrayAdapter(ResultActivity.this,android.R.layout.simple_list_item_1
,mTextFilenames));
}
public void finish(View view){
finish();
}
}
布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:text="OK"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="finish"/>
<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
LinearLayout>
通过setOnItemClickListener方法设置监听器,对用户的选择进行监听,并返回Conten URI和权限,有了URI之后,我们还需要返回一个能让其他app能读写该文件的权限,这个权限是临时的,当客户端app的任务栈结束,那么权限自动收回。
注意:setFlags方法是唯一安全授予临时权限的方法,要避免调用grantUriPermission方法,因为该方法只能通过调用revoleUriPermission方法取消权限。
listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
//当用户点击时,返回file的content URI 并且将它发送给请求的文件的app
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
File requestFile = mTextFiles[position];
Uri fileUri ;
try{
fileUri = FileProvider.getUriForFile(ResultActivity.this,
"com.zln.admin.fileprovider",requestFile);
if(fileUri != null){
//授予读取权限
mResultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
mResultIntent.setDataAndType(fileUri,getContentResolver().getType(fileUri));
ResultActivity.this.setResult(Activity.RESULT_OK,mResultIntent);
}else{
mResultIntent.setDataAndType(null,"");
ResultActivity.this.setResult(Activity.RESULT_CANCELED,mResultIntent);
}
}catch(IllegalArgumentException e){
Log.e("File Selector","The selected file can't be shared:"+mTextFilenames[position]);
}
}
});
到此,service app的编写已经完成,接下来为了验证是否文件共享成功,我们需要创建一个client app来验证。
client app的验证流程非常简单,通过隐式Intent请求共享文件Activity,选择共享文件,并在client app上显示出来即可。
public class MainActivity extends Activity {
private Intent mRequestFileIntent;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRequestFileIntent = new Intent(Intent.ACTION_PICK);
mRequestFileIntent.setType("text/plain");
}
public void requestFile(View view){
requestFile();
}
//当用户请求获取文件时,发送Intent
protected void requestFile(){
startActivityForResult(mRequestFileIntent,0);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(resultCode != RESULT_OK){
return;
}else{
Uri returnUri = data.getData();
try {
InputStream inputStream = getContentResolver().openInputStream(returnUri);
byte[] buff = new byte[1024];
int hasRead = inputStream.read(buff);
Log.e("MainActivity","hasRead"+hasRead);
if(hasRead != -1){
Toast.makeText(MainActivity.this,new String(buff,0,hasRead),Toast.LENGTH_SHORT).show();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Ok,让我们看看效果如何:
可以看到我们已经获取到文件的内容了。
Demo下载