共享文件

在将应用设置为使用内容 URI 共享文件后,您就可以响应其他应用对这些文件的请求。响应这些请求的一种方法是提供一个来自服务器应用的文件选择界面,其他应用可以调用该界面。这种方法允许客户端应用让用户从服务器应用中选择文件,然后接收所选文件的 content URI。

本课程将向您展示如何在应用中创建一个文件选择 Activity 来响应对文件的请求。

接收文件请求

要接收来自客户端应用对文件的请求并以 content URI 响应,您的应用应该提供一个文件选择 Activity。客户端应用通过调用带有 ACTION_PICK 操作的 IntentstartActivityForResult() 来启动此 Activity。当客户端应用调用 startActivityForResult() 时,您的应用可以以所选文件的 content URI 的形式将结果返回给客户端应用。

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

创建文件选择 Activity

要设置文件选择 Activity,首先在清单中指定 Activity,并添加一个与 ACTION_PICK 操作和 CATEGORY_DEFAULTCATEGORY_OPENABLE 类别匹配的意图过滤器。还要添加应用为其他应用提供服务的 MIME 类型过滤器。以下代码片段展示了如何指定新的 Activity 和意图过滤器

<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。由于 ActivityListView 中显示可用文件的列表,因此当用户点击文件名时,系统会调用方法 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> 元素中指定的权限。生成的 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());
                }
                ...
            }
        });
        ...
    }

请记住,您只能为位于元数据文件中指定的目录中的文件生成 content URI,该元数据文件包含 <paths> 元素,如 指定可共享目录 部分所述。如果您对未指定的路径中的 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();
    }

有关其他相关信息,请参阅