FileProvider控件的使用

导论

我们都知道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文件中,指定共享目录

  • 共享文件
    一旦你能使用Content URI来共享文件,那么就能对其他App的请求做出回应了,回应这些请求的方法就是提供一个client app可以调用的接口,通过这个方法可以在client app上让用户选择需要的文件,并且接收选择文件的Content URI。
    在接收文件请求之前,我们需要手动添加一个文件到我们的共享目录下,并在文件中写值,方便之后在client app中验证。
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();
                }
            }
        });
    }
}

FileProvider控件的使用_第1张图片
注意:在这里笔者被坑过,之前在该Activity读写文件时,我使用的是openFileInput(String filename) 方法来读取私有内存下的文件,该方法的参数是文件名,目录是在files目录下,并不是这个Demo中我们设定的files/text目录下的文件。因此读写的时候我们应该用FileInputStream/FileOutputStream指定路径进行读写。

  • 接收文件请求并回应
    为了接收client app的请求并回复一个Content URI,我们的app必须提供一个文件选择页面,client app通过startActivityForResult方法开启这个Activity,当client app使用startActivityForResult时,service app就可以返回用户选择的文件的Content URI了。
    创建一个文件选择Activity,为了响应请求,Activity要包含如下属性:
    Action:ACTION_PICK
    Category:CATEGORY_DEFAULT;CATEGORY_OPENABLE
    MIME type:共享文件的Mime Type.
<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,让我们看看效果如何:
FileProvider控件的使用_第2张图片
可以看到我们已经获取到文件的内容了。


Demo下载

你可能感兴趣的:(Android)