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
相关产品推荐
相关产品推荐

