分享

Content Provider(二)之 FileProvider 实现应用文件共享

 android之情殇 2017-05-25
由于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:name --- 定义名子 
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中
效果如下:


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



Sharing a File

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

    先看看效果

     

首先我们要有客户端的请求:  
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中特定的内容。
ContentResolver.openFileDescriptor(uri,"r") 得到是一个ParcelFileDescriptor,通过这个ParcelFileDescriptor.getFileDescriptor可以得到FileDescriptor,这个FileDescriptor就可以用来读取文件了

当然我们可以用这个uri做其它的事情 
例如 返回mime type 
String mimeType = getContentResolver().getType(returnUri);

    

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多