利用FileProvider建立文件分享

app经常需要给其他的app传送文件,比如QQ里面我们可能需要将图库里面的图片返回,比如图片浏览器想把图片文件传到图片编辑器中,或者文件管理器想让用户在external storage中复制粘贴文件。

为了将文件安全地从我们的应用程序共享给其它应用程序,唯一一种安全的方法就是将文件的URI传输给目标应用并授予该URI临时权限. 因为这权限是对于接收URI的目标应用有效,并且是临时的,会自动失效,所以这种方式是安全的(Android可以用FileProvider中的getUriForFile()来获取文件的URI)

为了传输文件,我们需要建立两个应用,一个应用来请求数据 启动另外一个应用,被启动的应用会共享它能共享 的文件,然后我们选择一个文件,确定后返回到原始应用,这个就会得到一个文件。

实例

下面看一个具体的实现:

用来请求数据的应用我们称之为客户端(Client),另外一个用于分享数据的应用我们称之为服务端(Server)

要实现的效果是:客户端请求数据,打开服务端,点击服务端中的图片返回到客户端。

设置文件分享

为了安全的提供一个文件让其他app访问,你需要将文件以URI的形式来提供出来.Android中有个类可以帮助我们,就是FileProvider,它能够基于xml中相应的配置生成相应的文件的URI。

step 1: 设置FileProvider

为了给应用程序定义一个FileProvider,需要在Manifest清单文件中定义一个entry,该entry指明了需要使用的创建Content URI的Authority。此外,还需要一个XML文件的文件名,该XML文件指定了我们的应用可以共享的目录路径。

下例展示了如何在清单文件中添加标签,来指定FileProvider类,Authority及XML文件名:

服务端(Service)

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.service">
    <application
        ...>
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.service.fileprovider"
            android:grantUriPermissions="true"
            android:exported="false">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>
        ...
    </application>
</manifest>

manifest中定义的时候有两点需要注意 :

  • android:authorities 字段指定了希望使用的Authority,该Authority针对于FileProvider所生成的content URI。本例中我的应用的包名为com.example.service对于自己的应用,要在我们的应用程序包名(android:package的值)之后继续追加“fileprovider”来指定Authority。

  • <provider>下的<meta-data>指向了一个XML文件,该文件指定了我们希望共享的目录路径。“android:resource”属性字段是这个文件的路径和名字(无“.xml”后缀)

step 2: 设置合适的路径

一旦在Manifest清单文件中为自己的应用添加了FileProvider,就需要指定我们希望共享文件的目录路径。为指定该路径,首先要在“res/xml/”下创建文件“filepaths.xml”。在这个文件中,为每一个想要共享目录添加一个XML标签。下面的是一个“res/xml/filepaths.xml”的内容样例。

服务端(Service)

<?xml version="1.0" encoding= "utf-8"?>
<resources>
    <paths >
        <files-path path="images/" name="myimages" />
    </paths >
</resources>

不知道怎么创建xml文件的可以查看这里

对于上面的代码有几点需要说明:

  • <files-path>标签共享的是在我们应用的内部存储中“files/”目录下的目录(等同于用getFilesDir()返回的路径)。

  • path属性的值表示的是前面的文件夹下的子文件,比如上例表示的是“files/images”

  • name属性表示的是与路径对应的在URI中的值。

  • <path>这个元素可以包含多个子标签,每一个都可以设置不同的分享路径,除了上例中使用的<files-path>之外,还可以使用<external-path>来分享external storage中的文件,还有<cache-path>用来分享internal storage中的cache文件。

有一点需要注意的是:

通过xml文件的方式来分享路径是唯一的方式,不能通过代码添加路径.

现在我们有一个完整的FileProvider声明,它在应用程序的内部存储中“files/”目录或其子目录下创建文件的Content URI。当我们的应用为一个文件创建了Content URI,该Content URI将会包含下列信息:

  • <provider>标签中指定的Authority(“com.example.myapp.fileprovider”);

  • 路径“myimages/”;

  • 文件的名字

比如本例子定义了一个FileProvider,然后我们需要一个文件“panda.jpg”的Content URI,FileProvider会返回如下URI:

content://com.example.service.fileprovider/myimages/panda.jpg

分享文件

通过上述设置好之后,你的app就可以响应其他app的文件请求了.而对于如何响应,一种方法是服务器app(也就是你的app)提供一个文件选择接口,让请求的app来调用,然后它就能通过该接口获得选中文件的URI.

step 1 : 接收文件请求

为了接收文件请求和返回相应的URI,你的app需要提供一个文件选择的Activity,客服端app通过调用startActivityForResult()并携带一个action为ACTION_PICK的intent来启动你的Activity,然后你做相应的处理并返回结果。

step 2 : 创建一个选择文件的Activity

为建立一个选择文件的Activity,首先需要在Manifest清单文件中定义Activity,在其Intent过滤器中,匹配ACTION_PICKActionCATEGORY_DEFAULTCATEGORY_OPENABLE这两种Category。另外,还需要为应用程序设置MIME类型过滤器,来表明我们的应用程序可以向其他应用程序提供哪种类型的文件。

下面这段代码展示了如何在清单文件中定义新的Activity和Intent过滤器:

服务端(Service)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ...
        <application>
        ...
             <activity android:name=".MainActivity"
            android:label="File Selector" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <action android:name="android.intent.action.PICK"/>
                <category android:name="android.intent.category.LAUNCHER" />


                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.OPENABLE"/>
                <data android:mimeType="text/plain"/>
                <data android:mimeType="image/*"/>
            </intent-filter>
        </activity>


    <provider>
        ...
    </provider>

step 3 : 在代码中定义文件选择Activity

定义一个Activity子类,用于显示在内部存储的“files/images/”目录下可以获得的文件,然后允许用户选择期望的文件。下面代码展示了如何定义该Activity,并令其响应用户的选择:

服务端(Service)

MainActivity.java :

我们会先创建一个文件,然后用ListView展示出来

public class MainActivity extends Activity {

   private File mPrivateRootDir;

    private File mImagesDir;

    File[] mImageFiles;
     //存放图片
    List<Bitmap> mBitmapList;
    //存放图片名称
    List<String> mBitmaoNamesList;

    private ListView listView;

    FileProviderAdapter myAdapter;

    boolean isOK;

    @Override
    protected void onCreate(Bundle savedInstanceState) {  
       super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        createInternalFiles();
        getBitmapsAndNames();
        listView = (ListView) this.findViewById(R.id.listView);
         myAdapter = new FileProviderAdapter(this, mBitmaoNamesList, mBitmapList);
        listView.setAdapter(myAdapter);
        ...
    }
   //我们用一个数组存放内部文件的名子和bitmap
 private void getBitmapsAndNames() {
        mImageFiles = mImagesDir.listFiles();

        mBitmapList = new ArrayList<Bitmap>();
        mBitmaoNamesList = new ArrayList<String>();
        for (int i = 0; i < mImageFiles.length; i++) {
            File image = mImageFiles[i];
            mBitmaoNamesList.add(image.getName());
            try {
                FileInputStream fis = new FileInputStream(image);
                Bitmap bitmap = BitmapFactory.decodeStream(fis);
                mBitmapList.add(bitmap);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }

        }
    }
    //内部存储中创建了一个文件panda.jpg,事先需要我们把panda.jpg文件放入mipmap中
    private void createInternalFiles() {
        mPrivateRootDir = getFilesDir();
        mImagesDir = new File(mPrivateRootDir, "images");
        if (!mImagesDir.exists()) {
            mImagesDir.mkdirs();
        }
        File mPandaIcon = new File(mImagesDir, "panda.jpg");
        Bitmap pandaBp = BitmapFactory.decodeResource(getResources(), R.mipmap.panda);
        saveFiles(mPandaIcon, pandaBp);
    }
      //保存文件
    private void saveFiles(File mPandaIcon, Bitmap pandaBp) {
        FileOutputStream fos = null;
        if (pandaBp != null) {
            try {
                fos = new FileOutputStream(mPandaIcon);
                pandaBp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

...

}

创建一个ListView的子项布局文件:

list_item.xml :

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:id="@+id/adpter_img"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:src="@mipmap/ic_launcher"/>
    <TextView
        android:id="@+id/adapter_txt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:paddingTop="15dp"
        android:textSize="18sp"
        android:textColor="@color/colorPrimaryDark"
        android:text="Hello"/>

</LinearLayout>

现在我们可以用ListView显示出来了,当然我们还要自己写一个Adapter**(FileProviderAdapte)**:

服务端(Service)

public class FileProviderAdapter extends BaseAdapter{
    private List<String> mData;
    private Context mContext;
    private LayoutInflater mLayoutInflater;
    private List<Bitmap> mBitmaps;
    public FileProviderAdapter(Context context, List<String> mBitmaoNamesList) {
        this.mContext = context;
        this.mData = mBitmaoNamesList;
        mLayoutInflater = LayoutInflater.from(mContext);
    }

    public FileProviderAdapter(Context context, List<String> mBitmaoNamesList, List<Bitmap> mBitmapList) {

        this.mContext = context;
        this.mData = mBitmaoNamesList;
        mLayoutInflater = LayoutInflater.from(mContext);
        this.mBitmaps = mBitmapList;
    }

    @Override
    public int getCount() {
        return mData.size();
    }

    @Override
    public Object getItem(int position) {
        return mData.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;
        if(convertView == null){
            viewHolder = new ViewHolder();
            convertView = mLayoutInflater.inflate(R.layout.list_item,null);
            viewHolder.img = (ImageView) convertView.findViewById(R.id.adpter_img);
            viewHolder.title = (TextView) convertView.findViewById(R.id.adapter_txt);
            convertView.setTag(viewHolder);
        }else{
            viewHolder = (ViewHolder) convertView.getTag();
        }
        if(mBitmaps != null){
            viewHolder.img.setImageBitmap(mBitmaps.get(position));
        }else{
            viewHolder.img.setBackgroundResource(R.mipmap.ic_launcher);
        }
        viewHolder.title.setText(mData.get(position));
        return convertView;
    }

    public final class ViewHolder{
        public ImageView img;
        public TextView title;
    }
}

接下来我们为listView定义点击事件。

step 4 : 响应一个文件选择

一旦用户选择了一个文件,你的应用就要决定具体是哪个文件并产生相应的URI,比如上例,我们把文件列在ListView中,当用户点击了某个文件,你可以在ListView的onItemClick()f方法中拿到相关信息,就可以知道是哪个文件.然后将该文件之前在<provider>中定义的FileProvider的authority以及Context这三个作为参数,调用getUriForFile()方法,就可以得到一个URI.如下示例:

protected void onCreate(Bundle savedInstanceState) {
        ...
 listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                File file = new File(mImagesDir, mBitmaoNamesList.get(position));
                Uri uri = FileProvider.getUriForFile(MainActivity.this, "com.example.service.fileprovider", file);
                Intent intent = new Intent("com.example.myapp.ACTION_RETURN_FILE");
                if (uri != null) {
        //给提供的分享文件授权
                    intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    intent.setDataAndType(uri, getContentResolver().getType(uri));
                    MainActivity.this.setResult(Activity.RESULT_OK, intent);
                } else {
                    intent.setDataAndType(null, "");
                    MainActivity.this.setResult(RESULT_CANCELED, intent);
                }

                isOK = true;
            }
        });
             ...
    }

记住你要生成的URI的文件必须是在meta-data中定义的路径下的文件,否则会报错。

setFlags最好的临时授权方式避免Context.grantUriPermission(),因为一旦调用这个方法,我们必须用Context.revokeUriPermission来取消这个权限, 而这个setFlags,只要这个activity所在的任务栈没被finish掉,临时权限就一起存在 ,也就是说如果你点back button一起返回finish这个任务栈,或者重启,这个权限就自动消失了。

当我们点击了这个item,我们需要一个button来让我们finish掉这个界面 ,我们在actionbar上定义了一个DONE。

在res/menu/中创建menu_main.xml:

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item android:id="@+id/action_done"
        android:title="DONE"
        app:showAsAction="ifRoom" />
</menu>

服务端(Service)

MainActivity.java:



    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        switch (id) {
            case R.id.action_done:
                if(isOk) {
                    finish();
                    isOk = false;
                }
            default:
                return super.onOptionsItemSelected(item);
        }
    }


现在我们已经完成了setResult工具,(设置一个Intent将相应参数传入,然后传入setResult()中,当该Activity结束的时候,客户端app就会收到这个Intent对象),我们再回到我们请求的Activity中:

请求分享一个文件

Client(客户端)

MainActivity.java:

首先我们要有客户端的请求, 点击按钮,弹出服务端应用:

 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        textView = (TextView)this.findViewById(R.id.textView);
        imageView = (ImageView)this.findViewById(R.id.client_img);
        button = (Button)this.findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mRequestFileIntent = new Intent(Intent.ACTION_PICK);
                mRequestFileIntent.setType("image/jpg");

                startActivityForResult(Intent.createChooser(mRequestFileIntent, "Get File"), 666);
            }
        });
    }

访问已请求的文件
客户端app在onActivityResult()方法中拿到服务器app返回的URI之后,就可以通过获取该文件的FileDescriptor访问该文件了.

客户端app唯一能够访问的文件就是拿到的URI的匹配文件,服务器app的其他文件它发现不了也打开不了,因为URI中不包含路径.

下面看具体处理示例:

 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode==600&&resultCode == RESULT_OK) {
            Log.d("david", "onActivityResult");
            Uri uri = data.getData();
            Cursor cursor = getContentResolver().query(uri, null, null, null, null);
            //move to fist cursor ,default is -1
            cursor.moveToNext();
            String name = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            String size = cursor.getString(cursor.getColumnIndex(OpenableColumns.SIZE));
            textView.setText("Name : " + name + "  Size : " + size);

            try {
                mInputPFD = getContentResolver().openFileDescriptor(uri, "r");
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                Log.e("MainActivity", "File not found.");
            }
            FileDescriptor fd = mInputPFD.getFileDescriptor();
            FileInputStream fis = new FileInputStream(fd);
            Bitmap bitmap = BitmapFactory.decodeStream(fis);
            imageView.setImageBitmap(bitmap);
        }
    }

获得临时授权的uri后,我们可以基本像访问content provider一样,访问这个uri中特定的内容。
ContentResolver.openFileDescriptor(uri,”r”) 得到是一个ParcelFileDescriptor,通过这个ParcelFileDescriptor.getFileDescriptor可以得到FileDescriptor,这个FileDescriptor就可以用来读取文件了

当然我们可以用这个uri做其它的事情
例如 返回mime type

String mimeType = getContentResolver().getType(returnUri);

效果展示:

这里写图片描述

这里写图片描述

放上Client应用MainActivity.java的完整代码:

public class MainActivity extends AppCompatActivity {
    private TextView textView;
    private ImageView imageView;
    private Button button;
    private Intent mRequestFileIntent;
    private ParcelFileDescriptor mInputPFD;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        textView = (TextView)this.findViewById(R.id.textView);
        imageView = (ImageView)this.findViewById(R.id.client_img);
        button = (Button)this.findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mRequestFileIntent = new Intent(Intent.ACTION_PICK);
                mRequestFileIntent.setType("image/jpg");

                startActivityForResult(Intent.createChooser(mRequestFileIntent, "Get File"), 666);
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode==600&&resultCode == RESULT_OK) {
            Log.d("david", "onActivityResult");
            Uri uri = data.getData();
            Cursor cursor = getContentResolver().query(uri, null, null, null, null);
            //move to fist cursor ,default is -1
            cursor.moveToNext();
            String name = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            String size = cursor.getString(cursor.getColumnIndex(OpenableColumns.SIZE));
            textView.setText("Name : " + name + "  Size : " + size);

            try {
                mInputPFD = getContentResolver().openFileDescriptor(uri, "r");
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                Log.e("MainActivity", "File not found.");
            }
            FileDescriptor fd = mInputPFD.getFileDescriptor();
            FileInputStream fis = new FileInputStream(fd);
            Bitmap bitmap = BitmapFactory.decodeStream(fis);
            imageView.setImageBitmap(bitmap);
        }
    }
}

放上Service应用MainActivity.java的完整代码:

public class MainActivity extends AppCompatActivity {

    private File mPrivateRootDir;
    // The path to the "images" subdirectory
    private File mImagesDir;
    // Array of files in the images subdirectory
    File[] mImageFiles;
    // Array of filenames corresponding to mImageFiles
    String[] mImageFilenames;
    List<Bitmap> mBitmapList;
    List<String> mBitmaoNamesList;
    private ListView listView;
    FileProviderAdapter myAdapter;
    boolean isOK;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        createInternalFiles();
        getBitmapsAndNames();
        listView = (ListView) this.findViewById(R.id.listView);
        myAdapter = new FileProviderAdapter(this, mBitmaoNamesList, mBitmapList);
        listView.setAdapter(myAdapter);

        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                File file = new File(mImagesDir, mBitmaoNamesList.get(position));
                Uri uri = FileProvider.getUriForFile(MainActivity.this, "com.example.service.fileprovider", file);
                Intent intent = new Intent("com.example.myapp.ACTION_RETURN_FILE");
                if (uri != null) {
                    intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    intent.setDataAndType(uri, getContentResolver().getType(uri));
                    MainActivity.this.setResult(Activity.RESULT_OK, intent);
                } else {
                    intent.setDataAndType(null, "");
                    MainActivity.this.setResult(RESULT_CANCELED, intent);
                }

                isOK = true;
            }
        });


    }

    private void getBitmapsAndNames() {
        mImageFiles = mImagesDir.listFiles();

        mBitmapList = new ArrayList<Bitmap>();
        mBitmaoNamesList = new ArrayList<String>();
        for (int i = 0; i < mImageFiles.length; i++) {
            File image = mImageFiles[i];
            mBitmaoNamesList.add(image.getName());
            try {
                FileInputStream fis = new FileInputStream(image);
                Bitmap bitmap = BitmapFactory.decodeStream(fis);
                mBitmapList.add(bitmap);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }

        }
    }

    private void createInternalFiles() {
        mPrivateRootDir = getFilesDir();
        mImagesDir = new File(mPrivateRootDir, "images");
        if (!mImagesDir.exists()) {
            mImagesDir.mkdirs();
        }
        File mPandaIcon = new File(mImagesDir, "panda.jpg");
        Bitmap pandaBp = BitmapFactory.decodeResource(getResources(), R.mipmap.panda);
        saveFiles(mPandaIcon, pandaBp);
    }

    private void saveFiles(File mPandaIcon, Bitmap pandaBp) {
        FileOutputStream fos = null;
        if (pandaBp != null) {
            try {
                fos = new FileOutputStream(mPandaIcon);
                pandaBp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        switch (id) {
            case R.id.action_done:
                if (isOK) {
                    finish();
                    isOK = false;
                }
            default:
                return super.onOptionsItemSelected(item);
        }
    }
}
  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值