共享文件

设置应用使用内容 URI 共享文件后,就可以响应其他应用对这些文件的请求。有一种方法可以响应这些请求,那就是从服务器应用提供一个其他应用可以调用的文件选择界面。通过这种方法,客户端应用可让用户从服务器应用选择文件,然后获得所选文件的内容 URI。

本节课介绍如何在应用中创建响应文件请求的文件选择 Activity

接收文件请求

要接收来自客户端应用的文件请求并使用内容 URI 进行响应,您的应用应提供一个文件选择 Activity。客户端应用使用包含 ACTION_PICK 操作的 Intent 来调用 startActivityForResult(),以此来启动该 Activity。当客户端应用调用 startActivityForResult() 时,您的应用可将用户所选的文件的内容 URI 作为结果返回给客户端应用。

要了解如何在客户端应用中实现文件请求,请参阅请求共享文件一课。

创建文件选择 Activity

要设置文件选择 Activity,请先在清单中指定以下内容:Activity、与操作 ACTION_PICK 匹配的 intent 过滤器以及类别 CATEGORY_DEFAULTCATEGORY_OPENABLE。另外,还需为您的应用向其他应用提供的文件添加 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
             */
             ...
        }
        ...
    }
    

响应文件选择

用户选择共享文件后,您的应用必须确定用户选择了哪个文件,然后为该文件生成内容 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(),同时传递您在 FileProvider<provider> 元素中指定的授权。所生成的内容 URI 包含授权、与文件目录对应的路径段(在 XML 元数据中指定)以及文件的名称(包括其扩展名)。如需了解 FileProvider 如何基于 XML 元数据将目录映射到路径段,请参阅指定可共享目录一节。

以下代码段展示了如何检测所选文件并为其生成内容 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());
                    }
                    ...
                }
            });
            ...
        }
    

请记住,您只能为特定的文件生成内容 URI,即位于包含 <paths> 元素的元数据文件中所指定的目录中的文件。如需了解如何指定该目录,请参见指定可共享目录一节。如果您针对未指定的路径中的 File 调用 getUriForFile(),则会收到 IllegalArgumentException

授予文件访问权限

您已经获得了要与其他应用共享的文件的内容 URI,现在要做的就是允许客户端应用访问该文件。要允许访问,您需要向客户端应用授予访问权限,方法是将该内容 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() 是利用临时访问权限安全地授予文件访问权限的唯一方法。请不要对文件的内容 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

与请求方应用共享文件

要与请求文件的应用共享文件,请将包含内容 URI 和权限的 Intent 传递给 setResult()。在您刚才定义的 Activity 执行完成后,系统会将包含内容 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();
        }
    

要了解更多相关信息,请参阅: