胡凯 · 更新于 2018-10-19 16:00:50

分享文件

编写:jdneo - 原文:http://developer.android.com/training/secure-file-sharing/sharing-file.html

对应用程序进行配置,使得它可以使用Content URI来共享文件后,其就可以响应其他应用程序的获取文件的请求了。一种响应这些请求的方法是在服务端应用程序提供一个可以由其他应用激活的文件选择接口。该方法可以允许客户端应用程序让用户从服务端应用程序选择一个文件,然后接收这个文件的Content URI。

本课将会展示如何在应用中创建一个用于选择文件的Activity,来响应这些获取文件的请求。

接收文件请求

为了从客户端应用程序接收一个文件获取请求并以Content URI的形式进行响应,我们的应用程序应该提供一个选择文件的Activity。客户端应用程序通过调用startActivityForResult()方法启动这一Activity。该方法包含了一个具有ACTION_PICKAction的Intent参数。当客户端应用程序调用了startActivityForResult(),我们的应用可以向客户端应用程序返回一个结果,该结果即用户所选择的文件所对应的Content URI。

关于如何在客户端应用程序实现文件获取请求,请参考:请求分享一个文件

创建一个选择文件的Activity

为建立一个选择文件的Activity,首先需要在Manifest清单文件中定义Activity,在其Intent过滤器中,匹配ACTION_PICKAction及CATEGORY_DEFAULTCATEGORY_OPENABLE这两种Category。另外,还需要为应用程序设置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,并令其响应用户的选择:

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
         */
         ...
    }
    ...
}

响应一个文件选择

一旦用户选择了一个被共享的文件,我们的应用程序必须明确哪个文件被选择了,并为该文件生成一个对应的Content URI。如果我们的ActivityListView中显示了可获得文件的清单,那么当用户点击了一个文件名时,系统会调用方法onItemClick(),在该方法中可以获取被选择的文件。

onItemClick()中,根据被选中文件的文件名获取一个File对象,然后将其作为参数传递给getUriForFile(),另外还需传入的参数是在<provider>标签中为FileProvider所指定的Authority,函数返回的Content URI包含了相应的Authority,一个对应于文件目录的路径标记(如在XML meta-data中定义的),以及包含扩展名的文件名。有关FileProvider如何基于XML meta-data将目录路径与路径标记进行匹配的知识,可以阅读:指定可共享目录路径

下例展示了如何检测选中的文件并且获得它的Content 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);
                }
                ...
            }
        });
        ...
    }

记住,我们能生成的那些Content URI所对应的文件,必须是那些在meta-data文件中包含<paths>标签的目录内的文件,这方面知识在Specify Sharable Directories中已经讨论过。如果调用getUriForFile()方法所要获取的文件不在我们指定的目录中,会收到一个IllegalArgumentException

为文件授权

现在已经有了想要共享给其他应用程序的文件所对应的Content URI,我们需要允许客户端应用程序访问这个文件。为了达到这一目的,可以通过将Content 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);
                }
                ...
             }
             ...
        });
    ...
    }

Caution:调用setFlags()来为文件授予临时被访问权限是唯一的安全的方法。尽量避免对文件的Content URI调用Context.grantUriPermission(),因为通过该方法授予的权限,只能通过调用Context.revokeUriPermission()来撤销。

与请求应用共享文件

为了向请求文件的应用程序提供其需要的文件,我们将包含了Content URI和相应权限的Intent传递给setResult()。当定义的Activity结束后,系统会把这个包含了Content URI的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);
                    }
                }
        });

当用户选择好文件后,我们应该向用户提供一个能够立即回到客户端应用程序的方法。一种实现的方法是向用户提供一个勾选框或者一个完成按钮。可以使用按钮的android:onClick属性字段为它关联一个方法。在该方法中,调用finish()。例如:

    public void onDoneClick(View v) {
        // Associate a method with the Done button
        finish();
    }