内容分享之文件分享

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中添加来匹配FileProvider,具体如下:


    
        
            
        
        ...
    

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

  1. Sharing Files
  2. Setting Up File Sharing
  3. Sharing a File
  4. Requesting a Shared File
  5. Retrieving File Information

你可能感兴趣的:(内容分享之文件分享)