app经常需要给其他的app传送文件,比如图片浏览器想把图片文件传到图片编辑器中,或者文件管理器想让用户在external storage中复制粘贴文件.
在所有的文件传输中,唯一一种安全的方法就是将文件的URI传输给目标应用并授予该URI临时权限. 因为这权限是对于接收URI的目标应用有效,并且是临时的,会自动失效,所以这种方式是安全的(Android可以用FileProvider中的getUriForFile()来获取文件的URI).
1. 设置文件分享
为了安全的提供一个文件让其他app访问,你需要将文件以URI的形式来提供出来.Android中有个类可以帮助我们,就是FileProvider,它能够基于xml中相应的配置生成相应的文件的URI.
1.1 设置FileProvider
FileProvider的定义需要在manifest中添加一个entry,这个entry会通过authority来产生URI,也就是在相应的xml文件中定义的app可以分享的路径.
下面代码将示范如何在manifest中添加
...
manifest中定义的时候有两点需要注意:
URI的形式为: scheme://authority/path/id
android:authorities这个属性适配的是你想要的URI的authority,比如上面的“com.example.myapp.fileprovider”.但是对于你在的app来说,你在manifest中的android:authorities的属性应该是你的包名再加上".fileprovider",比如你的包名是“com.you.demo",那么你的android:authorities应该填:"com.you.demo.fileprovider".具体可以看Content URIs和URI来了解详细的内容.
关于
中的android:resource属性的值是一个xml文件(直接拿文件名,不拿.xml扩展名).
1.2 设置合适的路径
一旦你在manifest中添加了FileProvider之后,你就需要再设置一个或多个要分享的文件的路径,而这需要三步:
i. 在res文件夹下建立一个xml文件夹.
ii. 在xml文件夹下建立一个文件(比如上例的filepaths.xml).
iii. 在相应的xml文件中设置路径.
如下示例:
对于上面的代码有几点需要说明:
- <files-path>这个tag表示分享的文件来自internal storage的files文件夹(等同于用getFilesDir()返回的路径),关于文件存储的详细内容可以之前的文件存储).
- path属性的值表示的是前面的文件夹下的子文件,比如上例表示的是"files/images"
- name属性表示的是与路径对应的在URI中的值.
-
这个元素可以包含多个子标签,每一个都可以设置不同的分享路径,除了上例中使用的 之外,还可以使用<external-path>来分享external storage中的文件,还有<cache-path>用来分享internal storage中的cache文件,更多的细节可以看Specifying Available Files.
有一点需要注意的是:
- 通过xml文件的方式来分享路径是唯一的方式,不能通过代码添加路径.
2 分享文件
通过上述设置好之后,你的app就可以响应其他app的文件请求了.而对于如何响应,一种方法是服务器app(也就是你的app)提供一个文件选择接口,让请求的app来调用,然后它就能通过该接口获得选中文件的URI.
2.1 接收文件请求
为了接收文件请求和返回相应的URI,你的app需要提供一个文件选择的Activity,客服端app通过调用startActivityForResult()并携带一个action为ACTION_PICK的intent来启动你的Activity,然后你做相应的处理并返回结果.
2.2 创建文件选择Activity
在manifes中的配置如下示例,要注意:
- action为ACTION_PICK.
- category要设置CATEGORY_DEFAULT和CATEGORY_OPENABLE两个.
- 然后MIME的类型是根据你提供的文件的类型而定.
...
...
...
2.3 文件选择Activity中的文件选择代码
public class MainActivity extends Activity {
// The path to the root of this app's internal storage
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;
// Initialize the Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// Set up an Intent to send back to apps that request a file
mResultIntent =
new Intent("com.example.myapp.ACTION_RETURN_FILE");
// Get the files/ subdirectory of internal storage
mPrivateRootDir = getFilesDir();
// Get the files/images subdirectory;
mImagesDir = new File(mPrivateRootDir, "images");
// Get the files in the images subdirectory
mImageFiles = mImagesDir.listFiles();
// Set the Activity's result to null to begin with
setResult(Activity.RESULT_CANCELED, null);
/*
* Display the file names in the ListView mFileListView.
* Back the ListView with the array mImageFilenames, which
* you can create by iterating through mImageFiles and
* calling File.getAbsolutePath() for each File
*/
...
}
...
}
2.4 响应文件选择操作
一旦用户选择了一个文件,你的应用就要决定具体是哪个文件并产生相应的URI,比如上例,我们把文件列在ListView中,当用户点击了某个文件,你可以在ListView的onItemClick()f方法中拿到相关信息,就可以知道是哪个文件.然后将该文件,之前在<provider>中定义的FileProvider的authority以及Context这三个作为参数,调起getUriForFile()方法,就可以得到一个URI.如下示例:
protected void onCreate(Bundle savedInstanceState) {
...
// Define a listener that responds to clicks on a file in the ListView
mFileListView.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
/*
* When a filename in the ListView is clicked, get its
* content URI and send it to the requesting app
*/
public void onItemClick(AdapterView> adapterView,
View view,
int position,
long rowId) {
/*
* Get a File for the selected file name.
* Assume that the file names are in the
* mImageFilename array.
*/
File requestFile = new File(mImageFilename[position]);
/*
* Most file-related method calls need to be in
* try-catch blocks.
*/
// Use the FileProvider to get a content URI
try {
fileUri = FileProvider.getUriForFile(
MainActivity.this,
"com.example.myapp.fileprovider",
requestFile);
} catch (IllegalArgumentException e) {
Log.e("File Selector",
"The selected file can't be shared: " +
clickedFilename);
}
...
}
});
...
}
记住你要生成的URI的文件必须是在meta-data中定义的路径下的文件,否则会报错(IllegalArgumentException).
2.5 给提供的分享文件授权
拿到URI之后,还需要让客户端App有访问该文件的权限. 可以通过将URI加入到Intent对象中,然后在给这个Intent对象设置权限标志,这个权限就会是临时的并且会在人物结束时自动失效. 如下示例:
protected void onCreate(Bundle savedInstanceState) {
...
// Define a listener that responds to clicks in the ListView
mFileListView.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> adapterView,
View view,
int position,
long rowId) {
...
if (fileUri != null) {
// Grant temporary read permission to the content URI
mResultIntent.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
...
}
...
});
...
}
注意:
- 上述通过Intent来调用addFlags()(或setFlag())来给文件授予访问权限是唯一的一种安全的方式.
- 别用Context的grantUriPermission() 方法来授权,因为该方法授权之后除非你用revokeUriPermission,否则无法撤消.
2.6 把文件分享给请求的App
设置一个Intent将相应参数传入,然后传入setResult()中,当该Activity结束的时候,客户端app就会收到这个Intent对象,如下示例:
protected void onCreate(Bundle savedInstanceState) {
...
// Define a listener that responds to clicks on a file in the ListView
mFileListView.setOnItemClickListener(
new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView> adapterView,
View view,
int position,
long rowId) {
...
if (fileUri != null) {
...
// Put the Uri and MIME type in the result Intent
mResultIntent.setDataAndType(
fileUri,
getContentResolver().getType(fileUri));
// Set the result
MainActivity.this.setResult(Activity.RESULT_OK,
mResultIntent);
} else {
mResultIntent.setDataAndType(null, "");
MainActivity.this.setResult(RESULT_CANCELED,
mResultIntent);
}
}
});
还可以给用户手动提供一个完成的操作:
public void onDoneClick(View v) {
// Associate a method with the Done button
finish();
}
3. 客户端App请求获取分享文件
上面讲的是服务器App的配置,现在说下客户端App的操作.
3.1 发送文件获取请求
如上<2.1 接收文件请求>提到过的客户端的请求操作,示例如下:
public class MainActivity extends Activity {
private Intent mRequestFileIntent;
private ParcelFileDescriptor mInputPFD;
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mRequestFileIntent = new Intent(Intent.ACTION_PICK);
mRequestFileIntent.setType("image/jpg");
...
}
...
protected void requestFile() {
/**
* When the user requests a file, send an Intent to the
* server app.
* files.
*/
startActivityForResult(mRequestFileIntent, 0);
...
}
...
}
3.2 访问已请求的文件
客户端app在onActivityResult()方法中拿到服务器app返回的URI之后,就可以通过获取该文件的FileDescriptor访问该文件了.
客户端app唯一能够访问的文件就是拿到的URI的匹配文件,服务器app的其他文件它发现不了也打开不了,因为URI中不包含路径.下面看具体处理示例:
/*
* When the Activity of the app that hosts files sets a result and calls
* finish(), this method is invoked. The returned Intent contains the
* content URI of a selected file. The result code indicates if the
* selection worked or not.
*/
@Override
public void onActivityResult(int requestCode, int resultCode,
Intent returnIntent) {
// If the selection didn't work
if (resultCode != RESULT_OK) {
// Exit without doing anything else
return;
} else {
// Get the file's content URI from the incoming Intent
Uri returnUri = returnIntent.getData();
/*
* Try to open the file for "read" access using the
* returned URI. If the file isn't found, write to the
* error log and return.
*/
try {
/*
* Get the content resolver instance for this context, and use it
* to get a ParcelFileDescriptor for the file.
*/
mInputPFD = getContentResolver().openFileDescriptor(returnUri, "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
Log.e("MainActivity", "File not found.");
return;
}
// Get a regular file descriptor for the file
FileDescriptor fd = mInputPFD.getFileDescriptor();
// Read the file
FileInputStream fileInputStream=new FileInputStream(fd);
...
}
}
上述中关键的方法是调用ContentResolver中的openFileDescriptor来获取文件的 ParcelFileDescriptor,然后就可以读取该文件了.
4. 获取文件信息
客户端app拿到URI之后,在处理文件之前,可以先请求获取文件的信息,包括文件的类型和大小,文件类型可以帮助客户端app来决定是否要处理该文件,文件大小用来决定文件buffering和caching的设置.
4.1 获取文件MIME类型
使用ContentResolver.getType()来获取,默认情况下FileProvider会根据文件的扩展名来决定文具店MIME类型,如下示例:
...
/*
* Get the file's content URI from the incoming Intent, then
* get the file's MIME type
*/
Uri returnUri = returnIntent.getData();
String mimeType = getContentResolver().getType(returnUri);
...
4.2 获取文件名和大小
ContentResolver中的query()方法会返回一个Cursor对象,包含指定文件的名字和大小,返回的Cursor对象默认的两列为:
- DISPLAY_NAME,文件名
- SIZE,文件大小(单位: byte),类型为long.
如下获取示例:
...
/*
* Get the file's content URI from the incoming Intent,
* then query the server app to get the file's display name
* and size.
*/
Uri returnUri = returnIntent.getData();
Cursor returnCursor =
getContentResolver().query(returnUri, null, null, null, null);
/*
* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data,
* and display it.
*/
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
returnCursor.moveToFirst();
TextView nameView = (TextView) findViewById(R.id.filename_text);
TextView sizeView = (TextView) findViewById(R.id.filesize_text);
nameView.setText(returnCursor.getString(nameIndex));
sizeView.setText(Long.toString(returnCursor.getLong(sizeIndex)));
...
Reference
- Sharing Files
- Setting Up File Sharing
- Sharing a File
- Requesting a Shared File
- Retrieving File Information