Search code examples
pythonandroidkivypyjniuspython-for-android

Downloaded files not appearing in the Downloads application in android kivy


I am trying to inform the download application that files have appeared in the Downloads folder, but after copying the file to this folder and using the code below, the file does not appear. For android version below 10 I get an error: jnius.jnius.JavaException: No methods matching your arguments, requested: (<java.lang.String at 0x796d9637c0 jclass=java/lang/String jself=<LocalRef obj=0x6516 at 0x796d9c6e10>>, <java.lang.String at 0x796d9637c0 jclass=java/lang/String jself=<LocalRef obj=0x6516 at 0x796d9c6e10>>, True, <java.lang.String at 0x796d963950 jclass=java/lang/String jself=<LocalRef obj=0x6502 at 0x796d957870>>, '/storage/emulated/0/Download/surface.stl', <java.lang.Integer at 0x796d963270 jclass=java/lang/Integer jself=<LocalRef obj=0x6522 at 0x796d957850>>, True), available: ['(Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;JZ)J', '(Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/String;JZLandroid/net/Uri;Landroid/net/Uri;)J']. I would some thing like MediaScannerConnection.scanFile.

from kivy.app import App
from kivy import platform
from plyer import storagepath
import mimetypes
import shutil
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
    Intent = autoclass('android.content.Intent')
    currentActivity = cast('android.app.Activity', activity)
    Build = autoclass('android.os.Build')
    ContentValues = autoclass('android.content.ContentValues')
    MediaStore = autoclass('android.provider.MediaStore')
    context = cast('android.content.Context', currentActivity.getApplicationContext())
    Downloads = autoclass('android.provider.MediaStore$Downloads')
    Environment = autoclass('android.os.Environment')
    Integer = autoclass('java.lang.Integer')
    String = autoclass('java.lang.String')
    File = autoclass('java.io.File')
    DownloadManager = autoclass('android.app.DownloadManager')
    Context = autoclass("android.content.Context")
    VERSION = autoclass('android.os.Build$VERSION')
    VERSION_CODES = autoclass('android.os.Build$VERSION_CODES')


class TestApp(App):
    def on_start(self):
        if platform == 'android':
            request_permissions([Permission.WRITE_EXTERNAL_STORAGE])

        self.add_file_to_download('test.txt')

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

        if not os.path.exists(file_in_download):
            shutil.copy(filename, file_in_download)
        else:
            print(f'File already in {download_dir}')

        file_size = os.path.getsize(file_in_download)
        mime_type = mimetypes.guess_type(file_in_download)[0]
        if not mime_type:
            mime_type = "text/plain"

        if platform == 'android':
            file_size = Integer(file_size)
            filename = String(filename)
            mime_type = String(mime_type)

            if VERSION.SDK_INT >= VERSION_CODES.Q:
                print('Use new solution')

                content_values = ContentValues()
                content_values.put(Downloads.TITLE, filename)
                content_values.put(Downloads.DISPLAY_NAME, filename)
                content_values.put(Downloads.MIME_TYPE, mime_type)
                content_values.put(Downloads.SIZE, file_size)

                # If  you downloaded to a specific folder inside "Downloads" folder
                # content_values.put(Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + File.separator + "Temp")

                database = context.getContentResolver()
                database.insert(Downloads.EXTERNAL_CONTENT_URI, content_values)

            else:
                print('Use old solution')

                downloader_manager = context.getSystemService(Context.DOWNLOAD_SERVICE)
                if downloader_manager:
                    downloader_manager.addCompletedDownload(filename, filename, True, mime_type, file_in_download,
                                                            file_size, True)

        print('File copied')


TestApp().run()

buildozer.spec

source.include_exts = py,png,jpg,kv,atlas,txt
android.permissions = WRITE_EXTERNAL_STORAGE,READ_EXTERNAL_STORAGE,DOWNLOAD_WITHOUT_NOTIFICATION
requirements = kivy,plyer,pyjnius,android
android.api = 32
android.ndk = 23b
p4a.branch = develop

I use this solution for java

EDIT:

Now old solution works fine for android 10 and below. On Android 10 new solution have no effect. And on Android S I get error: jnius.jnius.JavaException: JVM exception occurred: Invalid column null java.lang.IllegalArgumentException in this line resolver.insert(Downloads.EXTERNAL_CONTENT_URI, content_values)

from kivy.app import App
from kivy import platform
from plyer import storagepath
import mimetypes
import shutil
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
    Intent = autoclass('android.content.Intent')
    currentActivity = cast('android.app.Activity', activity)
    Build = autoclass('android.os.Build')
    ContentValues = autoclass('android.content.ContentValues')
    context = cast('android.content.Context', currentActivity.getApplicationContext())
    Downloads = autoclass('android.provider.MediaStore$Downloads')
    Environment = autoclass('android.os.Environment')
    Integer = autoclass('java.lang.Integer')
    String = autoclass('java.lang.String')
    File = autoclass('java.io.File')
    DownloadManager = autoclass('android.app.DownloadManager')
    Context = autoclass("android.content.Context")
    VERSION = autoclass('android.os.Build$VERSION')
    VERSION_CODES = autoclass('android.os.Build$VERSION_CODES')


class TestApp(App):
    def on_start(self):
        if platform == 'android':
            request_permissions([Permission.WRITE_EXTERNAL_STORAGE])

        self.add_file_to_download('test.txt')

    def add_file_to_download(self, path: str):
        """
        :param path: file near the main.py
        :return:
        """

        download_dir = str(storagepath.get_downloads_dir())
        filename, ext = os.path.splitext(path)
        full_file_name = filename + ext

        file_in_download = os.path.join(download_dir, full_file_name)

        if os.path.exists(file_in_download):
            os.remove(file_in_download)
            print(f'File already in {download_dir}')

        shutil.copy(full_file_name, file_in_download)

        file_size = os.path.getsize(file_in_download)
        mime_type = mimetypes.guess_type(file_in_download)[0]
        if not mime_type:
            mime_type = "text/plain"

        if platform == 'android':
            Integer_file_size = Integer(file_size)
            Integer_pending = Integer(1)

            if VERSION.SDK_INT >= VERSION_CODES.Q:
                print('Use new solution')
                
                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.SIZE, Integer_file_size)
                content_values.put(Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
                content_values.put(Downloads.IS_PENDING, Integer_pending)

                print(f'content_values {content_values}')

                # If  you downloaded to a specific folder inside "Downloads" folder
                # content_values.put(Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + File.separator + "Temp")

                database = context.getContentResolver()
                database.insert(Downloads.EXTERNAL_CONTENT_URI, content_values)
            else:
                print('Use old solution')

                downloader_manager = context.getSystemService(Context.DOWNLOAD_SERVICE)
                is_media_scanner_scannable = True
                notification = False

                if downloader_manager:
                    downloader_manager.addCompletedDownload(full_file_name,
                                                            full_file_name,
                                                            is_media_scanner_scannable,
                                                            mime_type,
                                                            file_in_download,
                                                            file_size,
                                                            notification,
                                                            )

        print('File copied')


TestApp().run()

Solution

  • FYI on Android >= 11 copying to the Downloads folder has no utility. Other apps can't read a file in Downloads even if they have READ_EXTERNAL_STORAGE. The traditional use of Downloads as a pool of shared files no longer works. Since only your app can read the file there is no point in putting it there. The idea is Downloads is only a cache for files your app downloaded.