You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android Kivy开发:Downloads目录文件未在系统下载应用显示

问题原因

三个核心问题导致报错和文件不显示:

  • Android 10以下版本报错:addCompletedDownload方法的文件大小参数要求是long类型,你传入了java.lang.Integer类型,方法签名不匹配直接触发异常,从报错信息里的可用方法签名(Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;JZ)J也能看到,第6个参数J就代表long类型。
  • Android 10版本新方案无效:你手动把文件复制到Downloads目录后再插入MediaStore记录,没有完成MediaStore要求的待写入状态流转,系统不会把你插入的记录和实际文件关联,自然不会在下载应用中显示。
  • Android 12+版本插入报错:MediaStore在高版本对必填字段校验严格,你缺少写入完成标记、必要的时间字段,直接抛出Invalid column null异常。
修复实现

适配逻辑

  • 权限:API < 29 申请WRITE_EXTERNAL_STORAGE、READ_EXTERNAL_STORAGE权限;API >=29 使用分区存储,不需要申请存储权限即可操作Downloads目录下的媒体记录。
  • 低版本(API <29):修正参数类型,文件大小转为java.lang.Long类型传入addCompletedDownload。
  • 高版本(API >=29):不要提前手动复制文件,遵循MediaStore写入流程:先插入带IS_PENDING=1的待写入记录拿到系统分配的Uri,通过Uri打开输出流写入文件内容,最后更新记录把IS_PENDING设为0,标记文件可用。

修复后完整代码

from kivy.app import App
from kivy import platform
from plyer import storagepath
import mimetypes
import os

if platform == "android":
    from jnius import autoclass, cast
    from android.permissions import request_permissions, Permission

    PythonActivity = autoclass('org.kivy.android.PythonActivity')
    activity = PythonActivity.mActivity
    currentActivity = cast('android.app.Activity', activity)
    ContentValues = autoclass('android.content.ContentValues')
    context = cast('android.content.Context', currentActivity.getApplicationContext())
    Downloads = autoclass('android.provider.MediaStore$Downloads')
    Environment = autoclass('android.os.Environment')
    Long = autoclass('java.lang.Long')
    FileInputStream = autoclass('java.io.FileInputStream')
    DownloadManager = autoclass('android.app.DownloadManager')
    Context = autoclass("android.content.Context")
    VERSION = autoclass('android.os.Build$VERSION')
    VERSION_CODES = autoclass('android.os.Build$VERSION_CODES')
    MediaScannerConnection = autoclass('android.media.MediaScannerConnection')
    System = autoclass('java.lang.System')


class TestApp(App):
    def on_start(self):
        if platform == 'android' and VERSION.SDK_INT < VERSION_CODES.Q:
            request_permissions([Permission.WRITE_EXTERNAL_STORAGE, Permission.READ_EXTERNAL_STORAGE])
        self.add_file_to_download('test.txt')

    def add_file_to_download(self, local_file_path: str):
        download_dir = str(storagepath.get_downloads_dir())
        full_file_name = os.path.basename(local_file_path)
        file_in_download = os.path.join(download_dir, full_file_name)

        # 先将源文件复制到缓存临时路径,避免高版本分区存储路径冲突
        tmp_file = os.path.join(context.getCacheDir().getPath(), full_file_name)
        with open(local_file_path, 'rb') as src, open(tmp_file, 'wb') as dst:
            dst.write(src.read())

        file_size = os.path.getsize(tmp_file)
        mime_type = mimetypes.guess_type(full_file_name)[0] or "application/octet-stream"

        if platform != 'android':
            # 非安卓平台直接复制文件即可
            import shutil
            if os.path.exists(file_in_download):
                os.remove(file_in_download)
            shutil.copy(tmp_file, file_in_download)
            print('File copied')
            return

        if VERSION.SDK_INT >= VERSION_CODES.Q:
            # 高版本分区存储适配
            content_values = ContentValues()
            content_values.put(Downloads.TITLE, full_file_name)
            content_values.put(Downloads.DISPLAY_NAME, full_file_name)
            content_values.put(Downloads.MIME_TYPE, mime_type)
            content_values.put(Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
            content_values.put(Downloads.IS_PENDING, 1)
            content_values.put(Downloads.DATE_ADDED, System.currentTimeMillis() // 1000)

            resolver = context.getContentResolver()
            insert_uri = resolver.insert(Downloads.EXTERNAL_CONTENT_URI, content_values)
            if not insert_uri:
                print('Insert media record failed')
                return

            # 写入文件内容到系统分配的Uri
            out_stream = resolver.openOutputStream(insert_uri)
            in_stream = FileInputStream(tmp_file)
            buf = [0]*4096
            while True:
                length = in_stream.read(buf)
                if length <=0:
                    break
                out_stream.write(buf, 0, length)
            in_stream.close()
            out_stream.close()

            # 更新状态为文件可用
            finish_values = ContentValues()
            finish_values.put(Downloads.IS_PENDING, 0)
            finish_values.put(Downloads.SIZE, file_size)
            resolver.update(insert_uri, finish_values, None, None)
        else:
            # 低版本适配
            import shutil
            if os.path.exists(file_in_download):
                os.remove(file_in_download)
            shutil.copy(tmp_file, file_in_download)
            download_manager = context.getSystemService(Context.DOWNLOAD_SERVICE)
            if download_manager:
                # 文件大小参数传Long类型,匹配方法签名
                download_manager.addCompletedDownload(
                    full_file_name,
                    full_file_name,
                    True,
                    mime_type,
                    file_in_download,
                    Long(file_size),
                    False
                )
            # 主动触发媒体扫描兜底
            MediaScannerConnection.scanFile(context, [file_in_download], None, None)
        
        # 清理临时文件
        os.remove(tmp_file)
        print('File copied')


TestApp().run()

极简通用方案(仅需刷新媒体库)

如果不需要把文件登记为下载管理器的正式下载记录,只需要实现MediaScannerConnection.scanFile的媒体库刷新效果,让文件能在系统文件管理器、相关应用中显示,可以直接在文件复制完成后调用扫描方法,全版本通用,不需要写复杂的分版本逻辑:

if platform == "android":
    # 文件复制到Downloads目录后直接调用即可
    MediaScannerConnection = autoclass('android.media.MediaScannerConnection')
    MediaScannerConnection.scanFile(context, [file_in_download], None, None)
buildozer.spec配置参考
source.include_exts = py,png,jpg,kv,atlas,txt
android.permissions = WRITE_EXTERNAL_STORAGE,READ_EXTERNAL_STORAGE
requirements = kivy,plyer,pyjnius,android
android.api = 33
android.ndk = 25b
p4a.branch = master

注意:API >=29 不需要额外申请存储权限,不要加多余的非必要权限,避免高版本权限校验异常。

内容的提问来源于stack exchange,提问作者Ne1zvestnyj

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 18:09:23