发布时间:2019/08/20 23:30:51   更新时间:2020/07/31 19:50:58
现在是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都正常了。