APP经常需要发送一个或多个文件给其他APP,例如,相册APP发送图片给图片编辑APP用来编辑图片,文件管理APP的复制和粘贴文件都涉及到文件分享功能。分享文件的唯一安全的方法是分享文件的content uri并临时授予访问权限给其他APP。这个拥有临时访问权限的content uri 是安全的,因为它只针对接收文件的APP授权,并且在访问完后权限自动失效。Android 系统提供了FileProvider
类帮助我们实现文件分享的功能,下面我们就具体讲述一下如何使用它实现文件分享功能。
为了实现安全的文件分享方式,我们需要通过Content uri 来分享文件。FileProvider
类可以帮助我们生成这些要分享的文件的URI。
FileProvider
类首先需要在AndroidManifest.xml
文件中注册FileProvider
类,并指定用于生成分享URI的Authority
,同时还要指定生成规则的xml文件。
<provider android:authorities="${package}.fileprovider" android:name="android.support.v4.content.FileProvider" android:grantUriPermissions="true" android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/filepaths"/>
</provider>
上述代码指定authority为APP包名加上“.fileprovider”后缀,使用meta-data
标签指定生成规则的xml文件。
生成规则的xml文件其实就是用来配置哪些目录下的文件是可以分享的,并为该目录指定一个字符串用于在content uri中唯一标识该目录。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<!-- APP internal storage 下的 images目录-->
<files-path path="images/" name="myimages"/>
<!-- APP external storage 下的 images目录-->
<external-path path="images/" name="externalimages"/>
<!-- APP 缓存目录 下的 images目录-->
<cache-path path="images" name = "cacheimages"/>
</paths>
上述配置文件将APP internal 存储和外部存储以及缓存目录下的image目录设置为共享目录。name
属性用于告诉FileProvider
为对应目录下的文件生成content uri时需要加上该字段。例如,文件”file/images/default_image.jpg”对应的生成的content uri为:
content://com.example.myapp.fileprovider/myimages/default_image.jpg
其中com.example.myapp.fileprovider
是authority
指定的值,myimages
是name
属性指定的值。
设置好分享目录之后,我们就可以响应其他APP的分享请求了。分享请求的Intent的action通常都是设置为ACTION_PICK
,接受度这样的请求后,通常的响应方式是提供一个文件选择界面让用户选择需要的文件,然后发送响应文件的content uri给他。
在AndroidManifest.xml
中注册用于处理分享请求的Activity。我们需要配置它的intent-filter,action 设置为ACTION_PICK
,category设置为android.intent.category.OPENABLE
,并配置可处理的数据类型为文本和图片类型。具体如下:
<activity android:name=".FileSelectActivity" android:label="选择文件">
<intent-filter>
<action android:name="android.intent.action.PICK"/>
<category android:name="android.intent.category.OPENABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
<data android:mimeType="image/*"/>
</intent-filter>
</activity>
这样我们就能接受文件分享的请求了,亦即我们的APP将会出现在选择列表里供用户选择。
接下来就是处理分享逻辑了。关键代码如下所示:
......
mFilesLv = (ListView)findViewById(R.id.lst_files);
String type = getIntent().getType();//获取请求的数据类型
File rootDir = getFilesDir();
if(TextUtils.equals("image/*",type)) {
//预处理要展示的图片
mImageDir = new File(rootDir, "images");
if (!mImageDir.exists()) {
mImageDir.mkdirs();
}
Log.d(TAG, "image dir=>" + mImageDir.getPath());
mFiles = mImageDir.listFiles();
if (mFiles != null) {
convertFilesToMap();
}
}else if(TextUtils.equals("text/plain",type)){
//TODO:处理文本文件的展示
}else{
//TODO:提示不支持的类型
}
//设置适配器
mFilesLv.setAdapter(new SimpleAdapter(this,mMapList,android.R.layout.simple_list_item_2,
new String[]{"name","size"},new int[]{android.R.id.text1,android.R.id.text2}));
mFilesLv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
File selectFile = mFiles[position];
//获取文件的URI
Uri fileUri = FileProvider.getUriForFile(FileSelectActivity.this,
"com.github.znacloud.contentsharedemo.fileprovider",selectFile);
Intent intent = new Intent();
intent.setDataAndType(fileUri,getContentResolver().getType(fileUri));
//授予临时权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
setResult(RESULT_OK,intent);
finish();
}
});
......
需要注意的是,如果用户选择的文件不在xml配置文件中配置的目录中,FileProvider#getUriForFile()
将会抛出异常IllegalArgumentException
异常。所以一定要确保文件列表中展示的文件都是在配置目录里的。
上面主要是讲述如何响应其他APP的分享请求。下面讲一讲如何主动发起请求去获取想要的文件,比如请求获取一张照片并上传到网络。
发起请求通常是构造一个Intent,intent的action 为ACTION_PICK
,并指定要请求的数据类型(MIME)。代码如下:
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent,REQUEST_PICK);
结果会在onActivityResult
回调方法中返回。当我们得到要获取的文件的URI后,通过URI可以获得文件描述符FileDescriptor
,有了文件描述符我们就可以访问文件内容了。前面讲过URI的授权问题,URI的授权是临时的,这个授权仅在当前APP进程的生命周期内有效。如果APP所在进程结束,则此次的授权失效,以后都不能再通过文件描述符直接访问文件,除非重新发起请求。获取文件描述符的代码如下:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(resultCode == RESULT_OK){
switch (requestCode){
case REQUEST_PICK:
Uri fileUri = data.getData();
String type = data.getType();
mPathEt.setText(fileUri.toString());
try {
ParcelFileDescriptor pfd =
getContentResolver().openFileDescriptor(fileUri, "r");
FileDescriptor fp = pfd.getFileDescriptor();
Log.d(TAG,"file desciptor=>"+fp.toString());
//如果为文本类型的文件
BufferedReader reader = new BufferedReader(new FileReader(fp));
String line;
while((line = reader.readLine()) != null){
Log.d(TAG,line);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
break;
default:break;
}
}
}
通常我们在准备读取文件时,先要了解文件的大概信息,比如文件名称、大小、文件类型等等。
文件类型可以直接通过URI来获取
Uri fileUri = returnIntent.getData();
String mimeType = getContentResolver().getType(fileUri);
文件的名称和大小信息则可以通过FileProvider
类获取到。FileProvider
类默认实现了query()
方法,该方法可以通过URI查询到文件的名字和大小信息,它返回两个字段信息:DISPLAY_NAME
和SIZE
,
DISAPLAY_NAME
对应的值等同于File#getName()
的返回值,SIZE
对应的值等同于File#length()
的返回值。代码如下:
Uri returnUri = returnIntent.getData();
Cursor returnCursor =
getContentResolver().query(fileUri, null, null, null, null);
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
returnCursor.moveToFirst();
String fileName = returnCursor.getString(nameIndex));
long fileSize = returnCursor.getLong(sizeIndex)));
使用NFC技术可以实现设备间传输文件。NFC全称Nearby Field Communication(近场通讯)。Android系统从4.0(API Level 14)开始支持NFC,它通过Android Beam NDEF transfer API
提供接口来支持NFC。
从4.1开始,Android Beam file transfer
支持在设备间传输大文件。为了发送大文件到其他设备,我们首先要声明使用NFC和外部存储的权限,还需要手机硬件支持NFC功能,同时必须提供要传输文件的URI。
Android Beam file transfer
具有如下特性:
FileProvider
生成的URI。权限声明如下:
<uses-permission android:name="android.permission.NFC" /><!-- NFC 权限-->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><!-- 读外存权限-->
声明NFC特性:通过生命nfc feature,并设置required为true,表明没有NFC功能,APP将不能正常工作。
<uses-feature android:name="android.hardware.nfc" android:required="true" />
此外,为了能够使用Android Beam file transfer
,需要设置androidMinSdkVerion=16
,若低于16,则应该在代码中检测API版本,区别处理:
...
// 不支持NFC
if (!PackageManager.hasSystemFeature(PackageManager.FEATURE_NFC)) {
//TODO:禁用NFC功能
...
// Android Beam file transfer 不可用
} else if (Build.VERSION.SDK_INT <
Build.VERSION_CODES.JELLY_BEAN_MR1) {
// 如果不可用,则不再继续
mAndroidBeamAvailable = false;
//TODO:禁用transfer
...
} else {
//如果都可用,那么可以做后续的工作了
mNfcAdapter = NfcAdapter.getDefaultAdapter(this);
...
}
当系统检测到需要传输文件到另外一台支持NFC的设备时(这个通常是通过设备接触来触发),NfcAdapter.CreateBeamUrisCallback
接口方法createBeamUris()
会被调用,方法返回一个URI的数组,表示要传输的文件的URI,Android Beam file transfer
会将这些URI代表的文件拷贝传输到另外一台设备。
@Override
public Uri[] createBeamUris(NfcEvent event) {
return mFileUris;
}
我们需要实现一个该接口的类,然后设置给Android Beam file transfer
:
mFileUriCallback = new FileUriCallback();
// 根据传输请求类型动态设置该回调
mNfcAdapter.setBeamPushUrisCallback(mFileUriCallback,this);
要传输的文件URI通常是scheme 为“file://”,并且确保文件的全局可读性。
private Uri[] mFileUris = new Uri[10];
String transferFile = "transferimage.jpg";
File extDir = getExternalFilesDir(null);
File requestFile = new File(extDir, transferFile);
requestFile.setReadable(true, false);
fileUri = Uri.fromFile(requestFile);
if (fileUri != null) {
mFileUris[0] = fileUri;
} else {
Log.e("My Activity", "No File URI available for file.");
}
另外一台设备接收完文件后会收到Android Beam file transfer
发出的广播,广播的Intent 的Action 为ACTION_VIEW
,data为最后收到的文件的URI。我们可以定义一个处理这样的通知的Activity,将接收到的文件展现出来。因为通常接收到的文件都是存储在同一目录下,所以通过最后一个文件的uri我们可以得到存储目录,展示该目录下的文件,具体方法就不赘述了。
好了,以上就是Android 文件分享功能的重点知识。