设置应用以使用内容 URI 共享文件后,您可以响应其他应用对这些文件的请求。响应这些请求的一种方法是提供来自服务器应用的文件选择界面,其他应用可以调用该界面。这种方法允许客户端应用让用户从服务器应用中选择文件,然后接收所选文件的 content URI。
本课程将向您展示如何在应用中创建一个文件选择Activity
以响应文件请求。
接收文件请求
要接收来自客户端应用的文件请求并使用 content URI 进行响应,您的应用应提供一个文件选择Activity
。客户端应用通过调用 startActivityForResult()
并使用包含操作 ACTION_PICK
的Intent
来启动此Activity
。当客户端应用调用 startActivityForResult()
时,您的应用可以以用户选择的文件的 content URI 形式向客户端应用返回结果。
要了解如何在客户端应用中实现文件请求,请参阅课程请求共享文件。
创建文件选择 Activity
要设置文件选择Activity
,首先在清单中指定Activity
,以及与操作 ACTION_PICK
和类别 CATEGORY_DEFAULT
和 CATEGORY_OPENABLE
匹配的 intent 过滤器。还可以为应用提供给其他应用的文件添加 MIME 类型过滤器。以下代码段显示了如何指定新的Activity
和 intent 过滤器
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> ... <application> ... <activity android:name=".FileSelectActivity" android:label="@File Selector" > <intent-filter> <action android:name="android.intent.action.PICK"/> <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>
在代码中定义文件选择 Activity
接下来,定义一个Activity
子类,该子类显示应用内部存储的 files/images/
目录中可用的文件,并允许用户选择所需的文件。以下代码段演示了如何定义此Activity
并响应用户的选择
Kotlin
class MainActivity : Activity() { // The path to the root of this app's internal storage private lateinit var privateRootDir: File // The path to the "images" subdirectory private lateinit var imagesDir: File // Array of files in the images subdirectory private lateinit var imageFiles: Array<File> // Array of filenames corresponding to imageFiles private lateinit var imageFilenames: Array<String> // Initialize the Activity override fun onCreate(savedInstanceState: Bundle?) { ... // Set up an Intent to send back to apps that request a file resultIntent = Intent("com.example.myapp.ACTION_RETURN_FILE") // Get the files/ subdirectory of internal storage privateRootDir = filesDir // Get the files/images subdirectory; imagesDir = File(privateRootDir, "images") // Get the files in the images subdirectory imageFiles = imagesDir.listFiles() // Set the Activity's result to null to begin with setResult(Activity.RESULT_CANCELED, null) /* * Display the file names in the ListView fileListView. * Back the ListView with the array imageFilenames, which * you can create by iterating through imageFiles and * calling File.getAbsolutePath() for each File */ ... } ... }
Java
public class MainActivity extends Activity { // The path to the root of this app's internal storage private File privateRootDir; // The path to the "images" subdirectory private File imagesDir; // Array of files in the images subdirectory File[] imageFiles; // Array of filenames corresponding to imageFiles String[] imageFilenames; // Initialize the Activity @Override protected void onCreate(Bundle savedInstanceState) { ... // Set up an Intent to send back to apps that request a file resultIntent = new Intent("com.example.myapp.ACTION_RETURN_FILE"); // Get the files/ subdirectory of internal storage privateRootDir = getFilesDir(); // Get the files/images subdirectory; imagesDir = new File(privateRootDir, "images"); // Get the files in the images subdirectory imageFiles = imagesDir.listFiles(); // Set the Activity's result to null to begin with setResult(Activity.RESULT_CANCELED, null); /* * Display the file names in the ListView fileListView. * Back the ListView with the array imageFilenames, which * you can create by iterating through imageFiles and * calling File.getAbsolutePath() for each File */ ... } ... }
响应文件选择
用户选择共享文件后,您的应用必须确定选择了哪个文件,然后为该文件生成 content URI。由于Activity
在ListView
中显示可用文件的列表,因此当用户点击文件名时,系统会调用方法onItemClick()
,您可以在其中获取所选文件。
使用 intent 将文件 URI 从一个应用发送到另一个应用时,必须小心获取其他应用可以读取的 URI。在运行 Android 6.0(API 级别 23)及更高版本的设备上执行此操作需要特别注意,因为该版本的 Android 中的权限模型发生了变化,特别是READ_EXTERNAL_STORAGE
变成了危险权限,接收应用可能缺少此权限。
考虑到这些因素,我们建议您避免使用Uri.fromFile()
,它存在一些缺点。此方法
- 不允许跨配置文件共享文件。
- 要求您的应用在运行 Android 4.4(API 级别 19)或更低版本的设备上具有
WRITE_EXTERNAL_STORAGE
权限。 - 要求接收应用具有
READ_EXTERNAL_STORAGE
权限,这将在 Gmail 等重要的共享目标上失败,因为它们没有该权限。
与其使用 Uri.fromFile()
,您可以使用 URI 权限 来授予其他应用访问特定 URI 的权限。虽然 URI 权限不适用于 Uri.fromFile()
生成的 file://
URI,但它们适用于与内容提供程序关联的 URI。 FileProvider
API 可以帮助您创建此类 URI。这种方法也适用于不在外部存储中,而是在发送 Intent 的应用程序本地存储中的文件。
在 onItemClick()
中,获取所选文件的名称对应的 File
对象,并将其作为参数传递给 getUriForFile()
,以及您在 <provider>
元素(用于 FileProvider
)中指定的权限。生成的 content URI 包含权限、对应于文件目录的路径段(如 XML 元数据中指定),以及包含文件扩展名的文件名。 FileProvider
如何根据 XML 元数据将目录映射到路径段,在 指定可共享目录 部分进行了描述。
以下代码片段演示了如何检测选定的文件并获取其 content URI。
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { ... // Define a listener that responds to clicks on a file in the ListView fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> /* * Get a File for the selected file name. * Assume that the file names are in the * imageFilename array. */ val requestFile = File(imageFilenames[position]) /* * Most file-related method calls need to be in * try-catch blocks. */ // Use the FileProvider to get a content URI val fileUri: Uri? = try { FileProvider.getUriForFile( this@MainActivity, "com.example.myapp.fileprovider", requestFile) } catch (e: IllegalArgumentException) { Log.e("File Selector", "The selected file can't be shared: $requestFile") null } ... } ... }
Java
protected void onCreate(Bundle savedInstanceState) { ... // Define a listener that responds to clicks on a file in the ListView fileListView.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 * imageFilename array. */ File requestFile = new File(imageFilename[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: " + requestFile.toString()); } ... } }); ... }
请记住,您只能为驻留在包含 <paths>
元素的元数据文件中指定的目录中的文件生成 content URI,如 指定可共享目录 部分所述。如果您为未指定的路径中的 File
调用 getUriForFile()
,则会收到 IllegalArgumentException
。
授予文件的权限
现在您有了要与其他应用共享的文件的 content URI,您需要允许客户端应用访问该文件。要允许访问,请通过将 content URI 添加到 Intent
并随后在 Intent
上设置权限标志来向客户端应用授予权限。您授予的权限是临时的,并在接收应用的任务栈完成时自动过期。
以下代码片段演示了如何设置文件的读取权限。
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { ... // Define a listener that responds to clicks on a file in the ListView fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> ... if (fileUri != null) { // Grant temporary read permission to the content URI resultIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) ... } ... } ... }
Java
protected void onCreate(Bundle savedInstanceState) { ... // Define a listener that responds to clicks in the ListView fileListView.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 resultIntent.addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION); } ... } ... }); ... }
注意: 调用 setFlags()
是使用临时访问权限安全地授予文件访问权限的唯一方法。避免对文件的 content URI 调用 Context.grantUriPermission()
方法,因为此方法授予的访问权限只能通过调用 Context.revokeUriPermission()
来撤销。
不要使用 Uri.fromFile()
。它强制接收应用拥有 READ_EXTERNAL_STORAGE
权限,如果您尝试跨用户共享,则根本无法工作,并且在低于 4.4(API 级别 19)的 Android 版本中,需要您的应用拥有 WRITE_EXTERNAL_STORAGE
权限。而且,非常重要的共享目标(例如 Gmail 应用)不具备 READ_EXTERNAL_STORAGE
权限,导致此调用失败。相反,您可以使用 URI 权限来授予其他应用访问特定 URI 的权限。虽然 URI 权限不适用于 Uri.fromFile()
生成的 file:// URI,但它们适用于与内容提供程序关联的 URI。无需为此专门实现自己的内容提供程序,您可以也应该使用 FileProvider
,如 文件共享 中所述。
与请求应用共享文件
要与请求文件的应用共享文件,请将包含 content URI 和权限的 Intent
传递给 setResult()
。当您刚刚定义的 Activity
完成时,系统会将包含 content URI 的 Intent
发送到客户端应用。以下代码片段演示了如何执行此操作。
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { ... // Define a listener that responds to clicks on a file in the ListView fileListView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> ... if (fileUri != null) { ... // Put the Uri and MIME type in the result Intent resultIntent.setDataAndType(fileUri, contentResolver.getType(fileUri)) // Set the result setResult(Activity.RESULT_OK, resultIntent) } else { resultIntent.setDataAndType(null, "") setResult(RESULT_CANCELED, resultIntent) } } }
Java
protected void onCreate(Bundle savedInstanceState) { ... // Define a listener that responds to clicks on a file in the ListView fileListView.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 resultIntent.setDataAndType( fileUri, getContentResolver().getType(fileUri)); // Set the result MainActivity.this.setResult(Activity.RESULT_OK, resultIntent); } else { resultIntent.setDataAndType(null, ""); MainActivity.this.setResult(RESULT_CANCELED, resultIntent); } } });
为用户提供一种在选择文件后立即返回客户端应用的方法。一种方法是提供一个复选标记或“完成”按钮。使用按钮的 android:onClick
属性将方法与按钮关联。在方法中,调用 finish()
。例如
Kotlin
fun onDoneClick(v: View) { // Associate a method with the Done button finish() }
Java
public void onDoneClick(View v) { // Associate a method with the Done button finish(); }
有关其他相关信息,请参阅