kivy在android7以上版本应用FileProvider

作者:清一

类别:kivy   

发布时间:2019/08/20 23:30:51   更新时间:2019/08/20 23:30:51


现在是2019年8月20日,这篇文章绝对是国内独一无二的。不知道百度什么时候才能收录,让大家都能看到。。。

 

问题:在安卓7以上的版本上,用kivy做的app,进行拍照保存等操作时,抛出FileUriExposedException 异常。

 

这是由于Android 7开始执行的StrictMode API 政策,禁止在你的应用外部公开 file:// URI。这个问题,由来已久,如果用java开发安卓app,网上解决办法很多。比较好的文章有:

https://blog.csdn.net/lmj623565791/article/details/72859156

https://developer.android.com/training/camera/photobasics.html
https://developer.android.com/reference/androidx/core/content/FileProvider.html

看这些就足够了。

 

本文集中解决的问题是:怎样在kivy开发安卓app的时候,解决这个问题。

 

本文基于采用的库是:plyer,使用的编译工具链是python-for-android(p4a)

(这里,有些人问,为什么我不用buildozer编译工具?其实,buildozer就是p4a的全自动化。buildozer的缺点在于,每个项目要重新下载一整套东西。深度定制、解决问题的话,使用p4a应该更好。复用也好一点。)

(再多说一句,解决一个综合性问题,需要懂python、kivy、plyer、pyjnius、android、java等等。但最好的办法就是看库的源码。很多库的文档,都不及时更新。看源码是最重要的。)

 

接下来说修改步骤:

 

1、修改plyer的camera.py文件。

修改位置是:

/root/.local/share/python-for-android/build/python-installs/yourapp/plyer/platforms/android/camera.py

修改代码及注释是:

def _take_picture(self, on_complete, filename=None):



    assert(on_complete is not None)

    self.on_complete = on_complete

    #on_complete的参数

    self.filename = filename

    android.activity.unbind(on_activity_result=self._on_activity_result)

    android.activity.bind(on_activity_result=self._on_activity_result)

    intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)



    #uri = Uri.parse('file://' + filename)

    #parcelable = cast('android.os.Parcelable', uri)
#保留原来的接口,filename是路径加文件名,且确保文件名唯一性。
#这里传入的路径必须和Environment.getExternalStorageDirectory()是一样的。建议采用plyer的storagepath.get_external_storage_dir()传入。


    name = os.path.basename(filename)

    #映射File类

    File = autoclass('java.io.File')

    #创建私有目录

    #需要WRITE_EXTERNAL_STORAGE权限。

    #getExternalFilesDir仅保留为应用程序的私密照片,限载程序后,会删除这些文件。

    #如果您将照片保存到提供的目录中 getExternalFilesDir(),则媒体扫描程序无法访问这些文件,因为它们对您的应用程序是私有的。

    storageDir = Environment.getExternalStorageDirectory()

    #storageDir = Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)

    #实例化File类,参数是目录和文件名。

    imageFile = File(storageDir, name)

    #创建文件

    imageFile.createNewFile()

    #使用getUriForFile返回content://URI。针对Android 7.0(API级别24)及更高版本的更新应用,file://在包边界上传递URI会导致FileUriExposedException。因此,我们现在提出一种存储图像的通用方法 FileProvider。

    #您需要配置FileProvider。在应用的清单中,向您的应用AndroidManifest添加提供商,确保权限字符串与第二个参数匹配getUriForFile(Context, String, File)。

    photoUri = FileProvider.getUriForFile(

        Context.getApplicationContext(),

        "qingju.com.cn.yourapp.fileprovider",

        imageFile

    )

    #content://qingju.com.cn.yourapp.fileprovider/external/xxx.jpg

    parcelable = cast('android.os.Parcelable', photoUri)

    #添加权限

    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)



    #意图函数

    intent.putExtra(MediaStore.EXTRA_OUTPUT, parcelable)

    #动作函数

    activity.startActivityForResult(intent, 0x123)

 

注意:filename是入参,也是on_complete的回调入参。这个目录,要和imageFile的生产目录是一个。只不过,前边是python产生的str,后边是java的对象。

 

2、添加权限。——相信你在遇到这个问题之前,已经添加过了吧。

两步:

代码中:

from android.permissions import request_permissions, Permission

#存储日志使用、相机照相并存储使用。有该权限默认有了READ_EXTERNAL_STORAGE权限。

#request_permissions([Permission.READ_EXTERNAL_STORAGE])

request_permissions([Permission.WRITE_EXTERNAL_STORAGE])

#发送api使用

request_permissions([Permission.INTERNET])

request_permissions([Permission.ACCESS_NETWORK_STATE])

 

p4a命令选项添加:

--permission WRITE_EXTERNAL_STORAGE \

--permission INTERNET \

--permission ACCESS_NETWORK_STATE

 

3、修改AndroidManifest。

 

修改位置:

/usr/local/lib/python3.6/dist-packages/pythonforandroid/bootstraps/sdl2/build/templates/AndroidManifest.tmpl.xml

修改内容:

<application

……

<!--android:authorities="${applicationId}.provider"-->

<provider

    android:name="android.support.v4.content.FileProvider"

    android:authorities="qingju.com.cn.yourapp.fileprovider"

    android:exported="false"

    android:grantUriPermissions="true">

    <meta-data

        android:name="android.support.FILE_PROVIDER_PATHS"

        android:resource="@xml/file_paths"></meta-data>

</provider>

 

</application>

 

注意AndroidManifest的authoities字段,必须匹配代码中的getUriForFile的第二个参数。

 

4、添加file_paths.xml(上边AndroidManifest的resource字段)

添加位置:

/usr/local/lib/python3.6/dist-packages/pythonforandroid/bootstraps/sdl2/build/src/main/res

新建xml/file_paths.xml

内容如下

<!--

<root-path/> 代表设备的根目录,等同于new File("/");

<files-path/> 代表内部存储空间应用私有目录下的 files/ 目录,等同于context.getFilesDir()

<cache-path/> 代表内部存储空间应用私有目录下的 cache/ 目录,等同于context.getCacheDir()

<external-path/> 代表外部存储空间根目录,等同于Environment.getExternalStorageDirectory(),等同于plyer的storagepath.get_external_storage_dir()。

<external-path name="external" path="yourapp_log" />Environment.getExternalStorageDirectory()/yourapp_log,其他同理。——这一条似乎不起作用。

<external-files-path>代表外部存储空间应用私有目录下的 files/ 目录,等同于context.getExternalFilesDirs()

<external-cache-path>代表外部存储空间应用私有目录下的 cache/ 目录,等同于getExternalCacheDirs()



要使用content://uri替代file://uri,那么,content://的uri如何定义呢?

需要一个虚拟的路径对文件路径进行映射,所以需要编写个xml文件,通过path以及xml节点确定可访问的目录,通过name属性来映射真实的文件路径。

-->



<paths xmlns:android="http://schemas.android.com/apk/res/android">

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

    <files-path name="files" path="" />

    <cache-path name="cache" path="" />

    <external-path name="external" path="" />

    <external-files-path name="external_file_path" path="" />

    <external-cache-path name="external_cache_path" path="" />

    <!--external-path name="my_images" path="./" /-->

</paths>

 

 

5、下载并添加support.v4包

 

安装Android Support Repository

./tools/bin/sdkmanager "extras;android;m2repository"

(到了这一步,android的sdk应该下载过了。再补充下载这个就好了。)

 

注意:build.gradle没有用,不用改。这个用于androd studio上增加aar。我们用不上。

 

添加办法:

 

p4a命令选项添加:

--add-aar /home/kivy_android_p4a/yourapp/source/aar/support-v4-24.1.1.aar \

 

注意:如果这个添加不成功,app会直接运行不起来,没有日志,只有用adb调试来看。

命令是:

adb logcat | grep "yourapp" | tee yourapp.log

 

实测如上方法:android6、android7、android8、android9都正常了。


本文属于原创文章,未经许可,任何媒体、公司或个人不得刊发或转载。

本文网址:https://www.pyfield.com/blog/?id=20