diff --git a/examples/dialogs/dialogs/app.py b/examples/dialogs/dialogs/app.py index 06f0e266bd..327586dbd1 100644 --- a/examples/dialogs/dialogs/app.py +++ b/examples/dialogs/dialogs/app.py @@ -43,6 +43,18 @@ def action_open_file_dialog(self, widget): except ValueError: self.label.text = "Open file dialog was canceled" + async def action_open_file_dialog_android(self, widget): + try: + selected_uri = '' + selected_uri = await self.app.main_window.open_file_dialog( + title="Choose a file", + multiselect=False) + self.label.text = "You selected: " + str(selected_uri) + except ValueError as e: + selected_uri = str(e) + self.label.text = selected_uri + print(str(selected_uri)) + def action_open_file_filtered_dialog(self, widget): try: fname = self.main_window.open_file_dialog( @@ -57,6 +69,9 @@ def action_open_file_filtered_dialog(self, widget): except ValueError: self.label.text = "Open file dialog was canceled" + def action_open_file_filtered_dialog_android(self, widget): + self.label.text = "file_types currently not supported by rubicon java" + def action_open_file_dialog_multi(self, widget): try: filenames = self.main_window.open_file_dialog( @@ -72,6 +87,18 @@ def action_open_file_dialog_multi(self, widget): except ValueError: self.label.text = "Open file dialog was canceled" + async def action_open_file_dialog_multi_android(self, widget): + try: + selected_uri = '' + selected_uri = await self.app.main_window.open_file_dialog( + title="Choose a file", + multiselect=True) + self.label.text = "You selected: " + str(selected_uri) + except ValueError as e: + selected_uri = str(e) + self.label.text = selected_uri + print(str(selected_uri)) + def action_select_folder_dialog(self, widget): try: path_names = self.main_window.select_folder_dialog( @@ -81,6 +108,17 @@ def action_select_folder_dialog(self, widget): except ValueError: self.label.text = "Folder select dialog was canceled" + async def action_select_folder_dialog_android(self, widget): + try: + selected_uri = '' + selected_uri = await self.app.main_window.select_folder_dialog("Choose a folder", + multiselect=False) + self.label.text = "You selected: " + str(selected_uri) + except ValueError as e: + selected_uri = str(e) + self.label.text = selected_uri + print(str(selected_uri)) + def action_select_folder_dialog_multi(self, widget): try: path_names = self.main_window.select_folder_dialog( @@ -91,6 +129,9 @@ def action_select_folder_dialog_multi(self, widget): except ValueError: self.label.text = "Folders select dialog was canceled" + async def action_select_folder_dialog_multi_android(self, widget): + self.label.text = "Multiple folder selection is not supported" + def action_save_file_dialog(self, widget): fname = 'Toga_file.txt' try: @@ -180,62 +221,101 @@ def startup(self): btn_question = toga.Button('Question', on_press=self.action_question_dialog, style=btn_style) btn_confirm = toga.Button('Confirm', on_press=self.action_confirm_dialog, style=btn_style) btn_error = toga.Button('Error', on_press=self.action_error_dialog, style=btn_style) - btn_open = toga.Button('Open File', on_press=self.action_open_file_dialog, style=btn_style) - btn_open_filtered = toga.Button( - 'Open File (Filtered)', - on_press=self.action_open_file_filtered_dialog, - style=btn_style - ) - btn_open_multi = toga.Button( - 'Open File (Multiple)', - on_press=self.action_open_file_dialog_multi, - style=btn_style - ) - btn_save = toga.Button('Save File', on_press=self.action_save_file_dialog, style=btn_style) - btn_select = toga.Button('Select Folder', on_press=self.action_select_folder_dialog, style=btn_style) - btn_select_multi = toga.Button( - 'Select Folders', - on_press=self.action_select_folder_dialog_multi, - style=btn_style - ) - btn_open_secondary_window = toga.Button( - 'Open Secondary Window', - on_press=self.action_open_secondary_window, - style=btn_style - ) - btn_close_secondary_window = toga.Button( - 'Close All Secondary Windows', - on_press=self.action_close_secondary_windows, - style=btn_style - ) + if toga.platform.current_platform == 'android': + btn_open = toga.Button('Open File', on_press=self.action_open_file_dialog_android, style=btn_style) + btn_open_filtered = toga.Button( + 'Open File (Filtered)', + on_press=self.action_open_file_filtered_dialog_android, + style=btn_style + ) + btn_open_multi = toga.Button( + 'Open File (Multiple)', + on_press=self.action_open_file_dialog_multi_android, + style=btn_style + ) + btn_select = toga.Button('Select Folder', + on_press=self.action_select_folder_dialog_android, style=btn_style) + btn_select_multi = toga.Button( + 'Select Folders', + on_press=self.action_select_folder_dialog_multi_android, style=btn_style + ) + else: + btn_open = toga.Button('Open File', on_press=self.action_open_file_dialog, style=btn_style) + btn_open_filtered = toga.Button( + 'Open File (Filtered)', + on_press=self.action_open_file_filtered_dialog, + style=btn_style + ) + btn_open_multi = toga.Button( + 'Open File (Multiple)', + on_press=self.action_open_file_dialog_multi, + style=btn_style + ) + btn_save = toga.Button('Save File', on_press=self.action_save_file_dialog, style=btn_style) + btn_select = toga.Button('Select Folder', on_press=self.action_select_folder_dialog, style=btn_style) + btn_select_multi = toga.Button( + 'Select Folders', + on_press=self.action_select_folder_dialog_multi, + style=btn_style + ) + btn_open_secondary_window = toga.Button( + 'Open Secondary Window', + on_press=self.action_open_secondary_window, + style=btn_style + ) + btn_close_secondary_window = toga.Button( + 'Close All Secondary Windows', + on_press=self.action_close_secondary_windows, + style=btn_style + ) btn_clear = toga.Button('Clear', on_press=self.do_clear, style=btn_style) # Outermost box - box = toga.Box( - children=[ - btn_info, - btn_question, - btn_confirm, - btn_error, - btn_open, - btn_open_filtered, - btn_save, - btn_select, - btn_select_multi, - btn_open_multi, - btn_open_secondary_window, - btn_close_secondary_window, - btn_clear, - self.label, - self.window_label - ], - style=Pack( - flex=1, - direction=COLUMN, - padding=10 + if toga.platform.current_platform == 'android': + box = toga.Box( + children=[ + btn_info, + btn_open, + btn_open_filtered, + btn_select, + btn_select_multi, + btn_open_multi, + btn_clear, + self.label, + self.window_label + ], + style=Pack( + flex=1, + direction=COLUMN, + padding=10 + ) + ) + else: + box = toga.Box( + children=[ + btn_info, + btn_question, + btn_confirm, + btn_error, + btn_open, + btn_open_filtered, + btn_save, + btn_select, + btn_select_multi, + btn_open_multi, + btn_open_secondary_window, + btn_close_secondary_window, + btn_clear, + self.label, + self.window_label + ], + style=Pack( + flex=1, + direction=COLUMN, + padding=10 + ) ) - ) # Add the content on the main window self.main_window.content = box diff --git a/examples/dialogs/pyproject.toml b/examples/dialogs/pyproject.toml index e1b180e3de..ba4fe478fa 100644 --- a/examples/dialogs/pyproject.toml +++ b/examples/dialogs/pyproject.toml @@ -14,7 +14,9 @@ author_email = "tiberius@beeware.org" formal_name = "Dialog Demo" description = "A testing app" sources = ['dialogs'] -requires = [] +requires = [ + 'c:/Projects/Python/Toga/src/core' +] [tool.briefcase.app.dialogs.macOS] @@ -40,5 +42,6 @@ requires = [ [tool.briefcase.app.dialogs.android] requires = [ - 'toga-android', + #'toga-android', + 'c:/Projects/Python/Toga/src/android' ] diff --git a/src/android/toga_android/app.py b/src/android/toga_android/app.py index 63346eac5d..8daa636ece 100644 --- a/src/android/toga_android/app.py +++ b/src/android/toga_android/app.py @@ -7,6 +7,7 @@ from .window import Window + # `MainWindow` is defined here in `app.py`, not `window.py`, to mollify the test suite. class MainWindow(Window): pass @@ -137,3 +138,4 @@ async def intent_result(self, intent): self.native.startActivityForResult(intent, code) await result_future return result_future.result() + diff --git a/src/android/toga_android/libs/activity.py b/src/android/toga_android/libs/activity.py index 379a290ea1..07849cb303 100644 --- a/src/android/toga_android/libs/activity.py +++ b/src/android/toga_android/libs/activity.py @@ -1,5 +1,7 @@ from rubicon.java import JavaClass, JavaInterface +Activity = JavaClass("android/app/Activity") + # The Android cookiecutter template creates an app whose main Activity is # called `MainActivity`. The activity assumes that we will store a reference # to an implementation/subclass of `IPythonApp` in it. diff --git a/src/android/toga_android/libs/android/net.py b/src/android/toga_android/libs/android/net.py new file mode 100644 index 0000000000..bca2c5a7ef --- /dev/null +++ b/src/android/toga_android/libs/android/net.py @@ -0,0 +1,3 @@ +from rubicon.java import JavaClass + +Uri = JavaClass("android/net/Uri") diff --git a/src/android/toga_android/window.py b/src/android/toga_android/window.py index 2ebfa068c6..d268477f0a 100644 --- a/src/android/toga_android/window.py +++ b/src/android/toga_android/window.py @@ -1,5 +1,8 @@ from . import dialogs +from .libs.activity import Activity from .libs.android import R__attr +from .libs.android.content import Intent +from .libs.android.net import Uri from .libs.android.util import TypedValue @@ -104,3 +107,89 @@ def stack_trace_dialog(self, title, message, content, retry=False): def save_file_dialog(self, title, suggested_filename, file_types): self.interface.factory.not_implemented('Window.save_file_dialog()') + + async def open_file_dialog(self, title, initial_uri, file_mime_types, multiselect): + """ + Opens a file chooser dialog and returns the chosen file as content URI. + Raises a ValueError when nothing has been selected + + :param str title: The title is ignored on Android + :param initial_uri: The initial location shown in the file chooser. Must be a content URI, e.g. + 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' + :type initial_uri: str or None + :param file_mime_types: The file types allowed to select. Must be MIME types, e.g. + ['application/pdf','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']. + Currently ignored to avoid error in rubicon + :type file_mime_types: list[str] or None + :param bool multiselect: If True, then several files can be selected + :returns: The content URI of the chosen file or a list of content URIs when multiselect=True. + :rtype: str or list[str] + """ + print('Invoking Intent ACTION_OPEN_DOCUMENT') + intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.setType("*/*") + if initial_uri is not None and initial_uri != '': + intent.putExtra("android.provider.extra.INITIAL_URI", Uri.parse(initial_uri)) + if file_mime_types is not None and file_mime_types != ['']: + # Commented out because rubicon currently does not support arrays and nothing else works with this Intent + # see https://github.com/beeware/rubicon-java/pull/53 + # intent.putExtra(Intent.EXTRA_MIME_TYPES, file_mime_types) + self.interface.factory.not_implemented( + 'Window.open_file_dialog() on Android currently does not support the file_type parameter') + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiselect) + selected_uri = None + result = await self.app.intent_result(intent) + if result["resultCode"] == Activity.RESULT_OK: + if result["resultData"] is not None: + selected_uri = result["resultData"].getData() + if multiselect: + if selected_uri is None: + # when the user selects more than 1 file, getData() will be None. Instead, getClipData() will + # contain the list of chosen files + selected_uri = [] + clip_data = result["resultData"].getClipData() + if clip_data is not None: # just to be sure there will never be a null reference exception... + for i in range(0, clip_data.getItemCount()): + selected_uri.append(str(clip_data.getItemAt(i).getUri())) + else: + selected_uri = [str(selected_uri)] + if selected_uri is None: + raise ValueError("No filename provided in the open file dialog") + return selected_uri + + async def select_folder_dialog(self, title, initial_uri=None, multiselect=False): + """ + Opens a folder chooser dialog and returns the chosen folder as content URI. + Raises a ValueError when nothing has been selected + + :param str title: The title is ignored on Android + :param initial_uri: The initial location shown in the file chooser. Must be a content URI, e.g. + 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' + :type initial_uri: str or None + :param bool multiselect: If True, then several files can be selected + :returns: The content tree URI of the chosen folder or a list of content URIs when multiselect=True. + :rtype: str or list[str] + """ + print('Invoking Intent ACTION_OPEN_DOCUMENT_TREE') + intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + if initial_uri is not None and initial_uri != '': + intent.putExtra("android.provider.extra.INITIAL_URI", Uri.parse(initial_uri)) + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiselect) + selected_uri = None + result = await self.app.intent_result(intent) + if result["resultCode"] == Activity.RESULT_OK: + if result["resultData"] is not None: + selected_uri = result["resultData"].getData() + if multiselect is True: + if selected_uri is None: + selected_uri = [] + clip_data = result["resultData"].getClipData() + if clip_data is not None: + for i in range(0, clip_data.getItemCount()): + selected_uri.append(str(clip_data.getItemAt(i).getUri())) + else: + selected_uri = [str(selected_uri)] + if selected_uri is None: + raise ValueError("No folder provided in the open folder dialog") + return selected_uri diff --git a/src/core/toga/window.py b/src/core/toga/window.py index 30ae797696..e51cb1ca9e 100644 --- a/src/core/toga/window.py +++ b/src/core/toga/window.py @@ -292,14 +292,18 @@ def open_file_dialog(self, title, initial_directory=None, file_types=None, multi """ This opens a native dialog where the user can select the file to open. It is possible to set the initial folder and only show files with specified file extensions. If no path is returned (eg. dialog is canceled), a ValueError is raised. + Args: - title (str): The title of the dialog window. - initial_directory(str): Initial folder displayed in the dialog. - file_types: A list of strings with the allowed file extensions. + title (str): The title of the dialog window (ignored on Android) + initial_directory(str): Initial folder displayed in the dialog. On Android, this needs to be a content URI, + e.g. 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' + file_types: A list of strings with the allowed file extensions. On Android, these must be MIME types, + e.g. ['application/pdf','application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']. multiselect: Value showing whether a user can select multiple files. Returns: - The absolute path(str) to the selected file or a list(str) if multiselect + The absolute path(str) to the selected file or a list(str) if multiselect. On Android, you will get back + content URIs. """ return self._impl.open_file_dialog(title, initial_directory, file_types, multiselect) @@ -307,12 +311,15 @@ def select_folder_dialog(self, title, initial_directory=None, multiselect=False) """ This opens a native dialog where the user can select a folder. It is possible to set the initial folder. If no path is returned (eg. dialog is canceled), a ValueError is raised. + Args: - title (str): The title of the dialog window. - initial_directory(str): Initial folder displayed in the dialog. - multiselect (bool): Value showing whether a user can select multiple files. + title (str): The title of the dialog window (ignored on Android) + initial_directory(str): Initial folder displayed in the dialog. On Android, this needs to be a content URI, + e.g. 'content://com.android.externalstorage.documents/document/primary%3ADownload%2FTest-dir' + multiselect (bool): Value showing whether a user can select multiple folders (ignored on Android) Returns: - The absolute path(str) to the selected file or None. + The absolute path(str) to the selected file or None. On Android, you will get back + content tree URIs. """ return self._impl.select_folder_dialog(title, initial_directory, multiselect)