Google Training 建立分享内容的APP ------ 分享文件

今天看了Google上如何分享文件的教程,内容主要涉及了客户APP向另一服务APP请求文件,服务APP如何处理封装Uri,然后客户APP如何处理返回资源。大概是这样一个流程。

一般来说,一个应用向另一个应用提供文件的唯一的安全的方式,就是向客户APP提供文件内容的URI和暂时的访问权限。这个方式很安全,因为服务APP只会给客户APP提供文件的URI,并且当客户APP获取结果后授权会自动失效。

如果你只想在APP之间分享一些小型的数据,你只需要把数据放进Intent,并对Intent进行合理的设置即可。内容见上一篇

这次内容分四部分:

  • 如何设置APP去分享文件
  • 服务APP分享文件
  • 客户APP请求文件
  • 客户APP检索文件信息

下面开始代码分析。


如何设置APP分享文件

为了安全的分享文件,你应该对分享的文件进行安全的处理,通过content URI的方式。根据你在XML资源文件里明确的路径,Android组件-FileProvider就能够产生文件content URI。

1.明确FileProvider

为了给APP定义FileProvider你需要去修改APP的Manifest文件,添加< provider >元素,在这个元素里,应该包含产生content URI的授权,以及定义你想分享文件路径的XML文件名。

官方给出如下示例:

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

android:authorities指定了你想要分享的content URI的URI 权限,在这里是android:authorities=”com.example.myapp.fileprovider”,对于你的APP来说,权限的值由APP完整的包名,以及”fileprovider” 组成。

< meta-data >指向一个XML文件,在这个XML文件里定义了你想要分享的文件的具体路径。android:resource里就是XML文件的路径和文件名,没有.xml后缀。

明确想要分享的文件

一旦你完成了FileProvider的manifest文件修改,你的下一步就是要明确你想要分享那些文件。首先我们要在res/xml的子目录下新建filepaths.xml文件,然后为每一个我们想要分享的文件添加< xml >标签。下面这个代码片段展示了如何去分享APP内部存储的files/目录下的子文件:

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

在这个例子里,< files-path >标签表明你想要分享APP的内部存储里的files文件夹,path属性表明你想要分享的是files文件夹下的images文件夹,name属性告诉FileProvider去添加 myimages路径片段到files/images content URI里。

< paths >元素里你能够添加很多子元素,每一个子元素都对应了一个你想要分享的具体的文件,除了使用 < files-path >标签,你还可以使用< external-path >去分享在外部存储中的文件,还可以用< cache-path >去分享在内部缓存目录下的文件。

注意:XML文件是指定要分享的文件夹的唯一方式,你不能用编码的方式添加。

这时,你就完成了分享文件的预先设置,如果现在你分享一个文件,那么它对应的content URI应该包含 authority ,文件路径和文件名。比如说你分享default_image.jpg这个文件,它对应的content URI如下所示:

content://com.example.myapp.fileprovider/myimages/default_image.jpg

分享文件

完成分享文件的设置后,我们就能响应其他APP的文件分享请求。一种响应的方式是在服务APP上给用户提供文件选择界面,这个界面能被其他APP触发,然后用户完成选择后,客户APP就收到了选中文件的content URI。

接受文件分享请求

为了接受请求并响应文件的content URI,你应该先定义一个文件选择Activity。客户APP先封装好Action为ACTION_PICK的Intent,然后调用startActivityForResult()方法,然后你的APP接受处理好,然后再把数据通过Intent返回给客户APP。

创建一个文件选择Activity

在创建前,我们应该先明确这个Acitivity的Manifest文件该怎么写。在intent filter里面设置action 为 ACTION_PICK,设置category为 CATEGORY_DEFAULT 和 CATEGORY_OPENABLE,还有你向客户APP提供的数据的MIME类型。官方给出了如下示例:

<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

这个Acitivity应该能展示files/images/里的文件资源,并且用户可以通过它挑选自己想要的文件,下面的代码展示了如何定义Acticity并响应用户的挑选:

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,通常如果用listview来展示文件资源,我们可以在onItemClick()里处理这个事情。

首先我们要获取选中的文件,然后把这个问价作为参数传递给getUriForFile()方法,这个方法需要三个参数,上下文,< provider >里的授权,选中的文件。

下面是官方示例:

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

注意了,你只能分享在meta-data里path元素下的那些文件,如果在getUriForFile()方法里传递一个路径没有明确的文件,你将会收到 IllegalArgumentException 异常。

准许访问文件的权限

获取到选中文件的content URI之后,我们还要允许客户APP有权限访问这个文件。通常的做法是,现在返回数据的Intent里面加入URI,然后再加入允许权限的permission flags,这个访问权限是暂时的,当客户APP完成任务之后资格权限就自动失效了。

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

和客户APP分享文件

最后一步就是把包含了content URI和权限的Intent传递给setIntent()函数,当你的Activity完成使命结束之后,系统就会把Intent返回给客户APP。

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

当用户完成选择之后,我们应该提供给用户返回客户APP的途径。通常的做法是添加一个checkmark或者是一个完成按钮,然后调用finish()方法。

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

客户APP请求文件

发送请求

为了获取请求,首先设置好请求的Intent,设置action为ACTION_PICK,不要忘记设置请求的数据MIME类型,然后吊桶startActivityForResult()方法,下面是官方示例:

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

访问分享到的文件

文件通过Intent的形式返回,所以我们应该在客户APP上覆写onActivityResult()方法。一旦我们取得了文件的content URL,我们技能通过FileDescriptor来访问文件。

服务APP的文件的安全性是可以保证的,因为content URI是返回给客户APP的唯一的数据。URI不包含文件的目录,所以客户APP不能访问服务APP其他的文件,只能通过暂时性的权限,去访问这个url对应的文件。

下面的代码演示了客户APP如何处理接收到的Intent,并通过content URIdedao FileDescriptor:

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

openFileDiscriptor()方法会返回一个对应文件的ParcelFileDescriptor,通过它我们调用getFileDescriptor()得到FileDescriptor,然后我们就能任意读取文件了。


检索文件信息

在客户APP拿到文件后,最后要先确定一个文件的大小和数据类型等信息在开始工作。文件的数据类型能让客户APP知道自己是否可以处理它,而文件的大小能帮助客户APP建立对于这个文件的缓冲区。

获取文件的MIME类型

文件的数据类型决定了APP是否能处理它。为了确定文件类型,我们可以掉ContentResolver.getType(),这方法取药传递文件的content uri,返回文件的MIME类型。下面的代码展示了客户APP如何获取文件类型:

...
    /*
     * 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);
    ...

获取文件的名称和大小

FileProvider类提供了一个默认的实现方法-query(),这个方法能返回文件的名称大小,需要提供文件的content URI,结果以Cursor的形式返回。这个默认的实现返回两列:

  • DISPLAY_NAME

    • 文件的名字,String类型,我们也可以通过File.getName()方法得到。
  • SIZE

    • 文件的大小,以byte为单位,我们可以通过File.length()得到相同的值。

我们通过把query()方法的参数设置为null,除了contentURI,我们就能直接获得这两个值。下面是官方代码示例,他还把它们都设置在了TextView里:

...
    /*
     * 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)));
    ...

好了,每次写完都晕头转向,等哪天用到了,再回来看你。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值