内容分享之文件分享

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

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <application
        ...>
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.myapp.fileprovider"
            android:grantUriPermissions="true"
            android:exported="false">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>
        ...
    </application>
</manifest>

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 URIsURI来了解详细的内容.

  • 关于<meta-data>中的android:resource属性的值是一个xml文件(直接拿文件名,不拿.xml扩展名).

1.2 设置合适的路径

一旦你在manifest中添加了FileProvider之后,你就需要再设置一个或多个要分享的文件的路径,而这需要三步:

i.  在res文件夹下建立一个xml文件夹.
ii. 在xml文件夹下建立一个文件(比如上例的filepaths.xml).
iii.    在相应的xml文件中设置路径.

如下示例:

<paths>
    <files-path path="images/" name="myimages" />
</paths>

对于上面的代码有几点需要说明:

  • <files-path>这个tag表示分享的文件来自internal storage的files文件夹(等同于用getFilesDir()返回的路径),关于文件存储的详细内容可以之前的文件存储).
  • path属性的值表示的是前面的文件夹下的子文件,比如上例表示的是"files/images"
  • name属性表示的是与路径对应的在URI中的值.
  • <path>这个元素可以包含多个子标签,每一个都可以设置不同的分享路径,除了上例中使用的<files-path>之外,还可以使用<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中的配置如下示例,要注意:

<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>
...

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);
                }
                ...
             }
             ...
        });
    ...
    }

注意:

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对象默认的两列为:

如下获取示例:

...
    /*
     * 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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352

推荐阅读更多精彩内容