我正在参加创作者训练营第6期,点击了解活动概况

前言

有部分同学只需是上传或许下载,只需用到了文件,不管三七二十一写个 FileProvider 再说。

不是每一种情况都需求运用 FileProvider 的,啥?你问行不可?有没有缺点?

这… 写了确实能够,没缺点!可是这没有必要啊。

假如不需求FileProvider就不需求界说啊,假如界说了重复的 FileProvider,还会导致清单文件兼并失利,需求处理抵触,然后引出又一个问题,处理 FileProvider 的抵触问题,当然这不是本文的重点,网上也有处理方案。

这儿咱们只运用 FileProvider 来说,剖析一下如下场景:

1.比方咱们下载文件到SD卡,当然咱们一般都下载到download目录下,那么运用这个文件,需求 FileProvider 吗?

不需求!因为他是同享文件夹中,并不是在沙盒中。

2.那咱们把文件保存到沙盒中,比方 getExternalFilesDir 。那么咱们运用这个沙盒中的文件,需求 FileProvider 吗?

3.看情况,假如仅仅把此文件上传到服务器,上传到云平台,也便是咱们自己App运用自己的沙盒,是不需求 FileProvider 的

4.假如是想运用体系翻开文件,或许传递给第三方App,那么是需求 FileProvider 的。

也便是说一般运用场景,咱们只需在自己App沙盒中的文件,需求给其他App操作的时分,咱们才需求运用 FileProvider 。

比较典型的比方是,下载Apk到自己的沙盒文件中,然后调用Android的Apk装置器去装置运用(这是一个独自的App),咱们就需求 FileProvider 。

或许咱们沙盒中的图片,需求发送到第三方的App里面展现,咱们需求 FileProvider 。

话不多说,咱们从惯例的运用与示例上来看看怎样运用,清楚它的一些小细节。

一、惯例运用与界说

一般来说没有什么特殊的需求,咱们运用体系自带的 FileProvider 类来界说即可。

咱们再清单文件注册咱们的FileProvider

    <provider
        android:authorities="com.guadou.kt_demo.fileprovider"
        android:name="androidx.core.content.FileProvider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_path">
        </meta-data>
    </provider>

特点的一些阐明:

  1. authorities 是符号咱们这个ContentProvider的仅有标识,是一个用于认证的暗号,咱们一般默认运用包名+fileprovider来界说。(能不能运用其他,能够,abcd都行,可是没必要)
  2. name 是详细的FileProvider类,假如是体系的,就用上面的这种,假如是自界说的,就写自界说FileProvider的全类名。
  3. exported 是否限制其他运用获取此FileProvider。
  4. grantUriPermissions 是否授权其他运用获取拜访Uri权限,一般为true。
  5. meta-data 和下面的 name 都是固定的写法,重点是 resource 需求自己完成规矩,界说哪些私有文件会被提供拜访。

看看咱们界说的file_path文件:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <root-path name="myroot" path="." />
    <external-path name="external_file" path="." />
    <files-path name="inner_app_file" path="." />
    <cache-path name="inner_app_cache" path="." />
    <external-files-path name="external_app_file" path="." />
    <external-files-path name="log_file" path="log" />
    <external-cache-path name="external_app_cache" path="." />
    <external-cache-path name="naixiao_img" path="pos" />
</paths>

特点的含义如下:

  1. root-path 从SD卡开端找 例如 storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg
  2. external-path 从外置SD卡开端 例如 Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg
  3. external-files-path 外置沙盒file目录 例如 pos/naixiao-1122.jpg (实在目录在 Android/data/com.guadou.kt_demo/cache/pos/)
  4. external-cache-path 外置沙盒cache目录 例如 naixiao-1122.jpg (实在目录在 Android/data/com.guadou.kt_demo/cache/)
  5. files-path 和上面的同理,仅仅在内置的data/data目录下面
  6. cache-path 和上面的同理,仅仅在内置的data/data目录下面

一共运用的就这么几个,咱们能够看到我的界说,它是能够重复界说的。

比我我用到的这两个,是的相同类型的能够界说多个,

 <external-cache-path name="external_app_cache" path="." />
 <external-cache-path name="naixiao_img" path="pos" />

假如我界说了两个同类型的 external-cache-path ,他们的 name 你能够随便取,叫abc都行,首要是path , 推荐咱们假如想露出根目录就运用点. , 假如想露出指定的目录就写对应的文件夹名称。

比我我现在有一个图片在这个目录下

storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg

经过 FileProvider 获取Uri 也是分优先顺序的。

比方我界说了pos的目录,那么打印如下:

打印Uri:content://com.guadou.kt_demo.fileprovider/naixiao_img/naixiao-1122.jpg

那咱们现在把pos的去掉,只需这个。

<external-cache-path name="external_app_cache" path="." />

那么打印就如下:

打印Uri:content://com.guadou.kt_demo.fileprovider/external_app_cache/pos/naixiao-1122.jpg

换了name,多了pos的途径。

那咱们都去掉呢?只保留外置SD卡和SD卡的规矩。

    <root-path name="myroot" path="." />
    <external-path name="external_file" path="." />

那么打印就如下:

打印Uri:content://com.guadou.kt_demo.fileprovider/external_file/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg

就走到了外置SD卡的规矩中去了。

那咱们再去掉外置卡的规矩。此刻界说如下

 <root-path name="myroot" path="." />

此刻打印如下:

打印Uri:content://com.guadou.kt_demo.fileprovider/myroot/storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg

能够看到它的匹配规矩是一层一层往上找的,那咱们再去掉SD卡的规矩呢。。。

那不就空了吗,此刻就崩溃报错了,这样是真拿不到Uri了…

运用示例:

提到这儿,咱们还没有真的运用 FileProvider ,下面咱们以一个图片实例为比方演示怎么发送到体系的App

    //测试FileProvider
    fun fileProvider1() {
        val drawable = drawable(R.drawable.chengxiao)
        val bd: BitmapDrawable = drawable as BitmapDrawable
        val bitmap = bd.bitmap
        FilesUtils.getInstance().saveBitmap(bitmap, "naixiao-1122.jpg")
        val filePath = FilesUtils.getInstance().sdpath + "naixiao-1122.jpg"
        YYLogUtils.w("文件原始途径:$filePath")
        val uri = FileProvider.getUriForFile(commContext(), "com.guadou.kt_demo.fileprovider", File(filePath))
        YYLogUtils.w("打印Uri:$uri")
        //到体系中找翻开对应的文件
        openFile(filePath, uri)
    }
    private fun openFile(path: String, uri: Uri) {
        //取得文件扩展名
        val extension: String = path.substring(path.lastIndexOf(".") + 1)
        //经过扩展名找到mimeType
        val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
        YYLogUtils.w("mimeType: $mimeType")
        try {
            //构造Intent,发动目的,交由体系处理
            startActivity(Intent().apply {
                //暂时赋予读写权限
                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
                //表示用其它运用翻开
                action = Intent.ACTION_VIEW
                //给Intent 赋值
                setDataAndType(uri, mimeType)
            })
        } catch (e: Exception) {
            e.printStackTrace()
            YYLogUtils.e("不能翻开这种类型的文件")
        }
    }

很简略的一个比方,咱们把drawable中的一个图片,保存到咱们私有沙盒目录中,目录为

文件原始途径:/storage/emulated/0/Android/data/com.guadou.kt_demo/cache/pos/naixiao-1122.jpg

咱们经过 FileProvider 拿到 content://开头的uri途径。然后经过Intent匹配找到关于的第三方App来接纳。

运转成果如下:

别滥用FileProvider了,Android中FileProvider的各种场景应用

翻开了体系自带的图片检查器,还能修改图片,检查信息等。

那么打印就如下:

打印Uri:content://com.guadou.kt_demo.fileprovider/external_app_cache/pos/naixiao-1122.jpg

content 是 scheme。

com.guadou.kt_demo.fileprovider 即为咱们在清单文件中界说的 authorities,即是咱们的FileProvider的仅有表示,在接纳的时分作为host。

这样封装之后,当其他的App收到这个Uri就无法从这些信息得知咱们的文件的实在途径,相对有安全保证。

其他场景中,比方沙盒中的Apk文件想要装置,也是相同的流程,咱们需求赋予读写权限,然后设置DataAndType即可。代码的注释很详细,咱们能够参阅参阅。

此刻咱们都是发送了一个Intent,让体系自己去匹配符合条件的Activity。那有没有可能咱们自己做一个App去匹配它。

这… 如同还真行。

二、能不能自界说接纳文件?

其实咱们仿制体系的App的做法,咱们在自界说的Activity中参加指定Filter即可,比方这儿我需求接纳图片,那么我界说如下的 intent-filter :

     <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".ReceiveImageActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="content" />
                <data android:scheme="file" />
                <data android:scheme="http" />
                <data android:mimeType="image/*" />
            </intent-filter>
        </activity>

都是一些固定的写法,咱们在Activity上指明,它能够接纳图片数据,此刻咱们再回到第一个App,发送图片,看看运转的作用:

别滥用FileProvider了,Android中FileProvider的各种场景应用

之前仍是图片检查器,现在能够选择咱们自己的App来接纳图片数据了,可是咱们怎么接纳数据呢?

其实都是一些固定的代码,首要是拿到input流,然后操作流的处理。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_receive_image)
        if (intent != null && intent.action == Intent.ACTION_VIEW) {
            val uri = intent.data
            YYLogUtils.w("uri: $uri")
            if (uri != null && uri.scheme != null && uri.scheme == "content") {
                val fis = contentResolver.openInputStream(uri)
                if (fis != null) {
                    val bitmap = BitmapFactory.decodeStream(fis)
                    //展现
                    if (bitmap != null) {
                        val ivReveiverShow = findViewById<ImageView>(R.id.iv_reveiver_show)
                        ivReveiverShow.setImageBitmap(bitmap)
                    }
                }
            }
        }
    }

最简略的做法,直接根据uri翻开输入流,然后咱们能够经过 BitmapFactory 就能够拿到 Bitmap了,就能展现图片到ImageView上面。

作用如图:

别滥用FileProvider了,Android中FileProvider的各种场景应用

甚至咱们拿到了 input 流,咱们还能对流进行copy 操作,把你的图片保存到我自己的沙盒目录中,例如:


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_receive_image)
        if (intent != null && intent.action == Intent.ACTION_VIEW) {
            val uri = intent.data
            YYLogUtils.w("uri: $uri")
            if (uri != null && uri.scheme != null && uri.scheme == "content") {
                val fis = contentResolver.openInputStream(uri)
                if (fis != null) {
                    val inBuffer = fis.source().buffer()
                    val outFile = File(getExternalFilesDir("xiaoxiao"), "naixiao5566.jpg")
                    outFile.sink().buffer().use {
                        it.writeAll(inBuffer)
                        inBuffer.close()
                    }
                    YYLogUtils.w("存放的途径:${outFile.absolutePath}")
                    //展现
                    val ivReveiverShow = findViewById<ImageView>(R.id.iv_reveiver_show)
                    ivReveiverShow.extLoad(outFile.absolutePath)
                }
            }
        }
    }

保存到自己的沙盒文件之后,咱们看一看作用:

别滥用FileProvider了,Android中FileProvider的各种场景应用

如同还真的能行,秀啊。

别滥用FileProvider了,Android中FileProvider的各种场景应用

那此刻有人还会有一个疑问,你这办法都是我自动的发送给他人去展现,去操作!这都不是事,关键是能不能让他人自动的来操作、玩弄我的沙盒文件?

比方我做的App想获取微信,支付宝这些他人的App的沙盒中的图片?行不可?有没有办法能够做到?

别滥用FileProvider了,Android中FileProvider的各种场景应用

这…,你别逗我了。

三、能不能自动查询对方的沙盒?

转头一想,如同还真行,有操作空间啊… 既然 FileProvider 是承继自 ContentProvider 。那凭什么咱们的App都能获取到他人App的数据库了,不能获取他人的沙盒文件呢?那数据库文件不也存在沙盒中么?

例如联系人App,咱们开发的第三方App能够经过 ContentProvider 获取到联系人App中的联系人数据,那么只需第三方的App界说好对应的 ContentProvider 我不就能获取到它沙盒的文件了吗?

提到就做,咱们先把FileProvider设置为可拜访

     <provider
        android:name=".MyFileProvider"
        android:authorities="com.guadou.kt_demo.fileprovider"
        android:exported="true"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_path">
        </meta-data>
    </provider>

是的,android:exported="true" 设置成功之后咱们直接经过 contentResolver 去查询不就好了吗?

先运转一下试试! 运转就崩了?

别滥用FileProvider了,Android中FileProvider的各种场景应用

什么鬼哦,看看FileProvider的代码,本来不答应敞开

别滥用FileProvider了,Android中FileProvider的各种场景应用

本来 FileProvider的 exported 和 grantUriPermissions 都是指定的写法,不能改动,并且不答应露出,不答应给其他App自动拜访!

这和咱们的需求不符合啊,我就要自动拜访,既然你不可,那我不必你行了吧!我承继 ContentProvider 行了吧!我自己完成文件获取、Cursor封装行了吧!

不皮了,其实咱们直接经过承继 ContentProvider 并且答应 exported ,然后咱们经过自己完成的query办法,回来指定的Cursor信息,就能够完成!

部分代码如下:

public class MyFileProvider extends ContentProvider {
    @Override
    public void attachInfo(Context context, ProviderInfo info) {
        super.attachInfo(context, info);
        mStrategy = getPathStrategy(context, info.authority);
    }
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        YYLogUtils.w("走到query办法");
        final File file = mStrategy.getFileForUri(uri);
        YYLogUtils.w("file:" + file);
        if (!file.exists()) {
            return null;
        }
        boolean directory = file.isDirectory();
        if (directory) {
            YYLogUtils.w("阐明是文件夹啊!");
            File[] files = file.listFiles();
            for (File childFile : files) {
                if (childFile.isFile()) {
                    String name = childFile.getName();
                    String path = childFile.getPath();
                    long size = childFile.length();
                    Uri uriForFile = mStrategy.getUriForFile(childFile);
                    YYLogUtils.w("name:" + name + " path:" + path + " size: " + size +" uriForFile:"+uriForFile);
                }
            }
            //自己遍历封装Cursor完成    
            return null;
        } else {
            YYLogUtils.w("阐明是文件啊!");
            if (projection == null) {
                projection = COLUMNS;
            }
            String[] cols = new String[projection.length];
            Object[] values = new Object[projection.length];
            int i = 0;
            for (String col : projection) {
                if (OpenableColumns.DISPLAY_NAME.equals(col)) {
                    cols[i] = OpenableColumns.DISPLAY_NAME;
                    values[i++] = file.getName();
                } else if (OpenableColumns.SIZE.equals(col)) {
                    cols[i] = OpenableColumns.SIZE;
                    values[i++] = file.length();
                }
            }
            cols = copyOf(cols, i);
            values = copyOf(values, i);
            final MatrixCursor cursor = new MatrixCursor(cols, 1);
            cursor.addRow(values);
            return cursor;
        }
    }
}

我简略的做了文件和文件夹的处理,并不完好,假如是文件咱们能够直接回来一个简略的cursor,假如是文件夹需求咱们自己拼接子文件的cursor并回来。

接下来咱们看看其他App怎么自动这些文件,在另一个App中咱们先加上权限:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.hongyegroup.receiver">
    <queries>
        <provider android:authorities="com.guadou.kt_demo.fileprovider" />
    </queries>
    ...
</manifest>    

然后咱们直接运用 contentResolver.query

  private fun queryFiles() {
        val uri = Uri.parse("content://com.guadou.kt_demo.fileprovider/external_app_cache/pos/naixiao-1122.jpg")
        val cursor = contentResolver.query(uri, null, null, null, null)
        if (cursor != null) {
            while (cursor.moveToNext()) {
                val fileName = cursor.getString(cursor.getColumnIndex("_display_name"));
                val size = cursor.getLong(cursor.getColumnIndex("_size"));
                YYLogUtils.w("name: $fileName  size: $size")
                Toast.makeText(this, "name: $fileName  size: $size", Toast.LENGTH_SHORT).show()
            }
            cursor.close()
        } else {
            YYLogUtils.w("cursor-result: 为空啊")
            Toast.makeText(this, "cursor-result: 为空啊", Toast.LENGTH_SHORT).show()
        }
    }

假如咱们知道它的指定文件Uri,咱们能够经过query查询到文件的一些基本信息。详细是哪些信息,需求对方提供和界说。

假如想操作对方的文件,由于咱们现已拿到了对方的Uri,咱们能够直接经过inputStream来操作,例如:

        val fis = contentResolver.openInputStream(uri)
        if (fis != null) {
            val inBuffer = fis.source().buffer()
            val outFile = File(getExternalFilesDir(null), "abc")
            outFile.sink().buffer().use {
                it.writeAll(inBuffer)
                inBuffer.close()
            }
            YYLogUtils.w("保存文件成功")
        }

这些都是简略的基本操作,重点是假如我不知道详细的文件呢?

我就想把对方App的沙盒中的文件夹下面的悉数文件都拿到,行不可?

行!只需对方App合作就行,例如:

     private fun queryFiles() {
        val uri = Uri.parse("content://com.guadou.kt_demo.fileprovider/external_app_cache/pos/")
        val cursor = contentResolver.query(uri, null, null, null, null)
        if (cursor != null) {
            while (cursor.moveToNext()) {
                val fileName = cursor.getString(cursor.getColumnIndex("_display_name"));
                val size = cursor.getLong(cursor.getColumnIndex("_size"));
                val uri = cursor.getString(cursor.getColumnIndex("uri"));
                val fileUri = Uri.parse(uri)
                //就能够运用IO或许BitmapFactory来操作流了
                YYLogUtils.w("name: $fileName  size: $size")
                Toast.makeText(this, "name: $fileName  size: $size", Toast.LENGTH_SHORT).show()
            }
            cursor.close()
        } else {
            YYLogUtils.w("cursor-result: 为空啊")
            Toast.makeText(this, "cursor-result: 为空啊", Toast.LENGTH_SHORT).show()
        }
    }

这样便是把对方外置SD卡下面的cache目录下的pos目录下的悉数文件拿到手,当然了,这个需求对方App封装对应的cursor才行哦。

打印的Log如下:

别滥用FileProvider了,Android中FileProvider的各种场景应用

只需对方封装的Cursor,咱们能够把名字,大小,uri等信息都封装到Cursor中,提供给对方获取。

总结

FileProvider的首要运用场景便是分享,把自己沙盒中的文件分享,自动提供给其他匹配的App去运用。

运用其他App的图片?查询了目前市场上的主流App,微信,支付宝,闲鱼,美团,等App,例如在保存文件的时分都没有存在自己的沙盒中了,都是默认在DCIM或Pictures中,并存入 MediaStore 保存到图库中。

这样就算公共目录,无需FileProvider,咱们直接经过 MediaStore 就能获取和运用。

而假如想自动拜访其他App的沙盒文件,则需求对方App全方位合作,一般用于自家App的全家桶之类的运用。相对来说相对运用场景比较少。

不是做不到,仅仅咱们觉得没有必要罢了,究竟界说和运用相对杂乱,并且有露出危险,被进犯的危险等。

本文悉数代码均以开源,源码在此。咱们能够点个Star重视一波。

好了,本期内容如有讹夺的当地,希望同学们能够指出沟通。假如有更好的办法,也欢迎咱们谈论区讨论。

假如感觉本文对你有一点点点的启发,还望你能点赞支撑一下,你的支撑是我最大的动力。

Ok,这一期就此完结。

别滥用FileProvider了,Android中FileProvider的各种场景应用