利用FileProvider建立文件分享

来源:互联网 发布:线性时间选择算法分析 编辑:程序博客网 时间:2024/06/05 15:30

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);        }    }}
2 0
原创粉丝点击