由于Android的安全机制 ,一个进程默认不能影响另外一个进程的,如读取私有数据 。 那么对于进程间的文件的共享 ,出于安全考虑,用FileProvider。FileProvider会基于manifest中的定义定义的一个xml文件(xml目录 下),为所有定义的文件生成content URIs,这样外部的应用在没有权限的情况下,可以通过授予临时权限的content uri,读取相应的文件。

FileProvider是v4 support中的类 , 就继承ContentProvider。



Specify the FileProvider


    既然FileProvider继承于ContentProvider,那么一些"增删查改"的方法就自动封装好了,我们用现成的即可。


    在Manifest.xml文件中定义,并用xml文件定义哪些文件可以对外提供


<provider

android:name="android.support.v4.content.FileProvider"

android:authorities="com.ckt.fileproviderservice"

android:exported="false"

android:grantUriPermissions="true">

<meta-data

android:name="android.support.FILE_PROVIDER_PATHS"

android:resource="@xml/filepath" />

</provider>

 ---   定义名子  


android:authorities -- 定义authority


android:exported="false"  对其它应用不可用


android:grantUriPermissions="true"  既然对其它应用不可用,只能授予content uri临时权限


<meta-data>中的 android:name="android.support.FILE_PROVIDER_PATHS" (这个名子是固定的)和


                                  android:resource 指向一个xml文件,这个xml文件定义了要共享文件的路径






xml/filepath.xml



<paths>

<files-path name="my_images" path="image/" />

 

<external-path name="my_external" />

 

<cache-path name="my_cache" />

</paths>


<files-path>  指向的是内部存储要共享的目录 


<external-path> 指向外部存储要共享的目录 


<cache-path> 指向缓存要共享的目录


其中name为我们自己定义 的名子,path为具体的路径


如果请求一个default_image.jpg的 content uri,FileProvider返回的content URI是这样的


content://com.ckt.fileproviderservice/my_images/default_image.jpg




Send Binary Content


    


final Uri uri = FileProvider.getUriForFile(this, AUTHORITY, file);

final Intent intent = new Intent(Intent.ACTION_SEND);

intent.setType("image/png");

intent.putExtra(Intent.EXTRA_STREAM, uri);

startActivity(intent);

这是引用的supportv4Demos中的一例子,可以看到共享一个二进制数据需要三步:


1、用Intent.ACTION_SEND


2、设置MIME type


3、把指向数据的uri放到Intent.EXTRA_STREAM中


效果如下:






FreeFileSync 共享文件夹 fileprovider 分享_android



像这样的分享模式,最常见的就是ActionBar上的共享。





Sharing a File


    上面说的是我主动share出去,如果我们在另外一个应用的主动请求这个文件呢,如何达到共享。 例如 ,QQ会进入图库来选择一张图片发送。那么如何做呢,首先需要一个QQ一样的应用,来请求数据 启动另外一个应用,被启动的应用会共享它能共享 的文件,然后我们选择一个文件,确定后返回到原始应用,这个就会得到一个文件。 这样说可能比较模糊,看看具体实现。




    先看看效果




     


FreeFileSync 共享文件夹 fileprovider 分享_FileProvider_02


首先我们要有客户端的请求:   


mGetFileBt.setOnClickListener(new View.OnClickListener() {

@Override

public void onClick(View v) {

Intent intent = new Intent(Intent.ACTION_PICK);

intent.setType("image/*");

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

}

});

发送一个Intent,action用Intent.ACTION_PICK,然后设置MIME type




相应的服务端的Manifest文件中定义相应的Intent Filter


<activity

android:name=".MainActivity"

android:label="@string/app_name"

android:theme="@style/AppTheme.NoActionBar">

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<action android:name="android.intent.action.PICK" />

 

<category android:name="android.intent.category.DEFAULT" />

<category android:name="android.intent.category.OPENABLE" />

<category android:name="android.intent.category.LAUNCHER" />

 

<data android:mimeType="image/*" />

</intent-filter>

</activity>

我这里为了方便,直接用了入口MainActivity,在intent-filter中加入


< action android : name = "android.intent.action.PICK" />


< category android : name = "android.intent.category.DEFAULT" />


< category android : name = "android.intent.category.OPENABLE" />


    Used to indicate that an intent only wants URIs that can be opened with openFileDescriptor(Uri, String). Openable URIs must support at least the columns defined in OpenableColumns when queried.

           < data android : mimeType = "image/*" />




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


    


private void createInternalFiles() {

mRootDir = getFilesDir();

mImagesDir = new File(mRootDir, "Images");

if (!mImagesDir.exists()) {

mImagesDir.mkdirs();

}

 

File mPandaIcon = new File(mImagesDir, "panda.png");

Bitmap mPandaBm = BitmapFactory.decodeResource(getResources(), R.mipmap.panda);

saveFiles(mPandaBm, mPandaIcon);

}

 

private void saveFiles(Bitmap bitmap, File file) {

FileOutputStream fos = null;

if (bitmap != null) {

try {

fos = new FileOutputStream(file);

bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);

} catch (FileNotFoundException e) {

e.printStackTrace();

}

}

}

我们在内部存储中创建了一个文件panda.png,然后把一个图片pand写到这个panda.png文件中,这样内部存储现在就有一个文件了。




我们用一个数组存放内部文件的名子和bitmap


private void getBitmapsAndNames() {

File[] images = mImagesDir.listFiles();

for (int i = 0; i < images.length; i++) {

File image = images[i];

mFileNames.add(image.getName());

try {

FileInputStream fis = new FileInputStream(image);

Bitmap bitmap = BitmapFactory.decodeStream(fis);

mFileBitmaps.add(bitmap);

} catch (FileNotFoundException e) {

e.printStackTrace();

}

}

}

现在所有的具备了, 我们可以用ListView显示出来了,当然我们还要自己写一个Adapter


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> data) {

this.mContext = context;

this.mData = data;

mLayoutInflater = LayoutInflater.from(mContext);

}

 

public FileProviderAdapter(Context context, List<String> data, List<Bitmap> bitmaps) {

this.mContext = context;

this.mData = data;

mLayoutInflater = LayoutInflater.from(mContext);

this.mBitmaps = bitmaps;

}

 

@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.adapter_layout,null);

viewHolder.img = (ImageView) convertView.findViewById(R.id.adapter_img);

viewHolder.title = (TextView) convertView.findViewById(R.id.adapter_tv);

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;

}

}


现在我们来展示 ,并定义item点击事件


mAdapter = new FileProviderAdapter(this, mFileNames, mFileBitmaps);

 

mListView.setAdapter(mAdapter);

 

mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {

@Override

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

File file = new File(mImagesDir, mFileNames.get(position));

Uri uri = FileProvider.getUriForFile(MainActivity.this, "com.ckt.fileprovidertest.imageprovider", file);

Intent intent = new Intent();

if (uri != null) {

intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

intent.setDataAndType(uri, getContentResolver().getType(uri));

setResult(888, intent);

} else {

intent.setDataAndType(null, "");

setResult(RESULT_CANCELED, intent);

}

isOk = true;

}

});

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



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


@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工具,我们再回到我们请求的Activity中


@Override

protected void onActivityResult(int requestCode, int resultCode, Intent data) {

if (requestCode == 666 && resultCode == 888) {

ParcelFileDescriptor fileDescriptor = null;

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));

mNameTv.setText("Name : " + name + " Size : " + size);

 

try {

fileDescriptor = getContentResolver().openFileDescriptor(uri, "r");

} catch (FileNotFoundException e) {

e.printStackTrace();

}

FileDescriptor fd = fileDescriptor.getFileDescriptor();

FileInputStream fis = new FileInputStream(fd);

Bitmap bitmap = BitmapFactory.decodeStream(fis);

mFileImg.setImageBitmap(bitmap);

}

}

获得临时授权的uri后,我们可以基本像访问content provider一样,访问这个uri中特定的内容。


ParcelFileDescriptor.getFileDescriptor可以得到FileDescriptor,这个 FileDescriptor就可以用来读取文件了




当然我们可以用这个uri做其它的事情 


例如 返回mime type 


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