在 Kivy 的 API 30 (Android 11) 中读取和写入文件

Posted

技术标签:

【中文标题】在 Kivy 的 API 30 (Android 11) 中读取和写入文件【英文标题】:Reading and writing files in API 30 (Android 11) in Kivy 【发布时间】:2021-04-12 10:02:46 【问题描述】:

我正在用 Python/Kivy 编写一个应用程序(最近使用的版本是 1.11.1,现在是 2.0.0)。前年,android 10 发布(又名 Andoid Q,api 29),2020 年 Android 11 发布(又名 Android R,api 30)。从这些版本开始,谷歌对文件的读写设置了限制,所以现在大家知道,文件只有在获得一定的权限后,在特定的地方,按一定的顺序才能读写(现在我们需要使用应用程序的个人文件夹,应用程序当然可以在其中“制造垃圾”,并且仅来自与 API >= 29 的设备的 ScopedStorage 相关的公共目录)。我们不能随意处理 /storage/emulated/0/sdcard 中的文件。

到底是什么顺序我真的无法理解。我查看了很多示例,但现在似乎没有人真正编写应用程序需要为用户提供对其项目文件的公共访问(共享或打开它们)。我不想认为 kivy 只是为 hello world、root-checkers、游戏和其他应用程序创建的,这些应用程序可以通过内部文件夹轻松获得。除了 api 30 刚刚命令开发人员将应用程序移动到 ScopedStorage。在我看来,我只是错过了一些东西。

清单包含 WRITE_EXTERNAL_STORAGEREAD_EXTERNAL_STORAGE。 我正在 Ubuntu 18 / buildozer 上构建,一切正常。但无论如何,当尝试从文件中获取数据时,出现错误13 Permission denied

根据我找到的最佳信息:https://github.com/niharika2810/ScopedStorageDemo/blob/master/app/src/main/java/com/sample/scopedstorage/activities/MainActivity.kthttps://developer.android.com/training/secure-file-sharing/request-file.htmlpython-for-android 包装器似乎没有考虑到 api 30 的最新更改,可以在 android studio 中如此轻松地使用? 我自己尝试使用 pyjnius 获取文件数据。就像我将 java 代码转换为 python,应用程序获得了权限,结果,当我尝试读取文件时,它仍然显示 Permission denied

问题是:如何从 python-kivy-p4a-jnius 包中的 API 30 的文本文件中获取数据,以便应用程序不提供#13 Permission denied 错误?

我的部分代码(主要使用来自 kivy 论坛的代码,其中一个人以最佳方式(我认为)使用 callback for startActivityForResult 在 python/kivy 下构建) :

main.py 开头:

if platform == 'android':
    from kivy.logger import Logger
    from kivy.clock import Clock

    from jnius import autoclass
    from jnius import cast

    from android import activity
    from android.permissions import Permission, request_permissions, check_permission

    Activity = autoclass('android.app.Activity')
    PythonActivity = autoclass("org.kivy.android.PythonActivity")
    Intent = autoclass('android.content.Intent')
    Uri = autoclass('android.net.Uri')
    File = autoclass('java.io.File')
    Env = autoclass('android.os.Environment')
    
    MediaStore_Images_Media_DATA = "_data"  # Value of MediaStore.Images.Media.DATA

    # Custom request codes
    RESULT_LOAD_DOC = 1

    def permissions_callback(permissions, results):
        print('inside permissions_callback')
        if all([res for res in results]):
            print('Got all permissions')
            permissions_granted = True
        else:
            print('Did not get all permissions')
    
    def get_permissions():
        request_permissions([
            Permission.WRITE_EXTERNAL_STORAGE,
            Permission.READ_EXTERNAL_STORAGE,
            Permission.INTERNET],
            permissions_callback)
    
    def user_select_doc(callback):
        """Open File chooser and call callback with absolute filepath of document user selected.
        None if user canceled.
        """
        
        currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
        context = cast('android.content.ContextWrapper', currentActivity.getApplicationContext())
        file_p = cast('java.io.File', context.getExternalFilesDir(Env.DIRECTORY_DOCUMENTS))
        
        def on_activity_result(request_code, result_code, intent):
            if request_code != RESULT_LOAD_DOC:
                Logger.warning('user_select_doc: ignoring activity result that was not RESULT_LOAD_DOC')
                return

            if result_code == Activity.RESULT_CANCELED:
                Clock.schedule_once(lambda dt: callback(None), 0)
                return

            if result_code != Activity.RESULT_OK:
                # This may just go into the void...
                raise NotImplementedError('Unknown result_code ""'.format(result_code))

            selectedFile = intent.getData();  # Uri
            filePathColumn = [MediaStore_Images_Media_DATA]; # String[]
            # Cursor
            cursor = currentActivity.getContentResolver().query(selectedFile, filePathColumn, None, None, None)
            cursor.moveToFirst()

            # int
            columnIndex = cursor.getColumnIndex(filePathColumn[0]);
            # String
            docPath = cursor.getString(columnIndex);
            cursor.close();
            Logger.info('android_ui: user_select_doc() selected %s', docPath)

            Clock.schedule_once(lambda dt: callback(docPath), 0)
        
        activity.bind(on_activity_result = on_activity_result)
        
        intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
        intent.addCategory(Intent.CATEGORY_OPENABLE)
        intent.setType("*/*")
        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, False)
        intent.setAction(Intent.ACTION_GET_CONTENT)
        
        # currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
        # chooser = Intent.createChooser(intent, 'Select TXT')
        # currentActivity.startActivityForResult(chooser, RESULT_LOAD_DOC)
        currentActivity.startActivityForResult(intent, RESULT_LOAD_DOC)

    def load(filename):
        with io.open(filename, encoding='utf-8') as file:
            data = None
            
            try:
                data = json.load(file)
            except:
                print('JSON not loaded')
                return False

还有一些按钮:

def on_load(self, button):
    if platform == 'android':
        if check_permission("android.permission.WRITE_EXTERNAL_STORAGE") \
        and check_permission("android.permission.READ_EXTERNAL_STORAGE") \
        and check_permission("android.permission.INTERNET"):
        # if permissions_granted:   # variant
            user_select_doc(load)
        else:
            get_permissions()

【问题讨论】:

But anyway when trying to get data from a file, 文件的完整路径是什么?那个文件是怎么放在那里的?为什么不从尝试在某处写入文件开始? How can I get data from a text file 用户是如何选择该文件的? 不要使用 .DATA 列来获取该 uri 的文件路径。总是一个坏习惯,但 .DATA 在 Android 11 上没用。而是直接使用 uri 打开输入流,然后从中读取。根本不需要权限。 @blackapps 我的应用需要同时在桌面和安卓系统上运行。例如,用户正在桌面上处理项目并将项目写入 project.json。他把这个文件移到了他的智能手机上。例如到公共文件夹 /storage/emulated/0/Documents/project.json (我试图从中读取文件)。现在 android 应用程序应该打开/选择这个文件并解析项目。关于 kivy/android 中的 inputstream 我不太了解。如何使用 kivy/p4a/jnius(???) 创建输入流?我没有找到这样的信息。感谢您的任何回答 PS:对不起英语 @blackapps 我了解android的基本结构。但是我对android studio的命令结构了解不多(关于.DATA)。如果我知道,我可能不会使用 kivy) 【参考方案1】:

阅读问题似乎解决了。我不太清楚 ContentResolver 有什么丰富的内容。感谢 blackapps 提示查看的方向。 使用 currentActivity.getContentResolver().openInputStream(intent.getData()) 我能够获取所选文件的二进制数据,现在我可以对这些数据做任何我想做的事情。 因此,目前我的阅读解决方案(除了上面的代码)是:

# From the received Uri I am getting a data stream
selectedUri = intent.getData()
docStream = currentActivity.getContentResolver().openInputStream(selectedUri)

# I get an int array from the stream
ints = []
intVal = docStream.read()

while intVal != -1:
    ints.append(intVal)
    intVal = docStream.read()

# And convert the array to bytes and pass it to the callback function
docBytes = bytes(b % 256 for b in ints)
Clock.schedule_once(lambda dt: callback(docBytes), 0)

保存文件的解决方案(回到同一个文件或通过“另存为”)结果让我有点困惑。不是没有 FileDescriptor。下面是调用android保存窗口的函数(本质上只是改变了intent):

def android_dialog_save_doc(save_callback):
    currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
    
    def on_activity_save(request_code, result_code, intent):
        if request_code != RESULT_SAVE_DOC:
            Logger.warning('android_dialog_save_doc: ignoring activity result that was not RESULT_SAVE_DOC')
            return
        
        if result_code == Activity.RESULT_CANCELED:
            Clock.schedule_once(lambda dt: save_callback(None), 0)
            return
        
        if result_code != Activity.RESULT_OK:
            # This may just go into the void...
            raise NotImplementedError('Unknown result_code ""'.format(result_code))
        
        selectedUri = intent.getData()                  # Uri
        filePathColumn = [MediaStore_Images_Media_DATA] # String
        
        # Cursor
        cursor = currentActivity.getContentResolver().query(selectedUri, filePathColumn, None, None, None)
        cursor.moveToFirst()
        
        # If you need to get the document path, but I used selectedUri.getPath()
        # columnIndex = cursor.getColumnIndex(filePathColumn[0])  # int
        # docPath = cursor.getString(columnIndex)                 # String
        cursor.close()
        Logger.info('android_ui: android_dialog_save_doc() selected %s', selectedUri.getPath())
        
        Clock.schedule_once(lambda dt: save_callback(selectedUri), 0)
    
    activity.bind(on_activity_result = on_activity_save)
    
    # Here's another Intent in contrast to get the file
    intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
    intent.addCategory(Intent.CATEGORY_OPENABLE)
    intent.setType('*/*')
    
    currentActivity.startActivityForResult(intent, RESULT_SAVE_DOC)

作为android_dialog_save_doc(android_do_saving)传递的回调函数:

def android_do_saving(uri):
    currentActivity = cast('android.app.Activity', PythonActivity.mActivity)
    
    # Just some function that returns binary data from somewhere to write to uri
    bytedata = get_bytes()
    
    if bytedata != None:
        try:
            # For writing, it is important to get ParcelFileDescriptor from ContentResolver ...
            pfd = currentActivity.getContentResolver().openFileDescriptor(uri, "w")
            
            # ... because from ParcelFileDescriptor you need to use getFileDescriptor() function,
            # which will allow us to create a FileOutputStream (and not a regular OutputStream), ...
            fos = FileOutputStream(pfd.getFileDescriptor())
            
            # ... because FileOutputStream can access the OutputStream channel,
            fos_ch = fos.getChannel()
            
            # ... so that after writing data to a file ...
            fos.write(bytedata)
            
            # ... this channel was able to cut off extra bytes if the number of newly written bytes was less than in the file being rewritten.
            fos_ch.truncate(len(bytedata))
            
            fos.close()
            
            # I save the uri in case of a quick overwrite, so you do not open every time the android file selection window
            openedUri = uri
            
            print('Saving bytedata succeeded.')
        except:
            print('Saving bytedata failed.')

【讨论】:

你保存并获得了那个文件路径? @koushik 是的,我做到了。我使用了“selectedUri.getPath()”。但是可以使用“docPath = cursor.getString(columnIndex)”获取路径(见上文)。但是据我记忆,这些uri的字符串会有所不同,并且在API 30中仅通过uri字符串加载文件并不容易。 一切都很好,但是当从流中获取数组时非常慢,因为文件是 15 - 20 MB 的数据,所以转换成数组时需要这么多时间

以上是关于在 Kivy 的 API 30 (Android 11) 中读取和写入文件的主要内容,如果未能解决你的问题,请参考以下文章

Windows上的Kivy独立android apk

如何在 python/kivy/pyjnius 检测 Android 中的屏幕分辨率?

Python、Kivy 和 Android 游戏 [关闭]

从 kivy/python 程序创建适用于 Android 的 APK [重复]

python, kivy, geopy, buildozer

Kivy/pyjnius:获取我的应用程序的 android.app.Application 对象