diff --git a/android/src/toga_android/dialogs.py b/android/src/toga_android/dialogs.py index 9aa610f4d3..5487f2e2fa 100644 --- a/android/src/toga_android/dialogs.py +++ b/android/src/toga_android/dialogs.py @@ -1,10 +1,14 @@ +import abc + from android import R from android.app import AlertDialog -from android.content import DialogInterface +from android.content import DialogInterface, Intent from java import dynamic_proxy import toga +from .libs import utilfile + class OnClickListener(dynamic_proxy(DialogInterface.OnClickListener)): def __init__(self, fn=None, value=None): @@ -118,6 +122,20 @@ def __init__( self.native = None +class HandlerFileDialog(abc.ABC): + """An abstract class that handles file manager calls""" + + def __init__(self, parent): + self.parent = parent + self.app = toga.App.app._impl + self.mActive = toga.App.app._impl.native + + @abc.abstractmethod + def show(self): + """Запуск менеджера""" + pass + + class SaveFileDialog(BaseDialog): def __init__( self, @@ -132,7 +150,16 @@ def __init__( self.native = None +class HandlerOpenDialog(HandlerFileDialog): + def show(self): + intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.setType("*/*") + self.app.start_activity(intent, on_complete=self.parent.handler) + + class OpenFileDialog(BaseDialog): + def __init__( self, title, @@ -140,10 +167,14 @@ def __init__( file_types, multiple_select, ): - super().__init__() - - toga.App.app.factory.not_implemented("dialogs.OpenFileDialog()") - self.native = None + self.native = HandlerOpenDialog(self) + self.mActive = self.native.mActive + + def handler(self, code, indent): + uri = indent.getData() + content = self.mActive.getContentResolver() + reader = utilfile.PathReader(content, uri) + self.future.set_result(reader) class SelectFolderDialog(BaseDialog): diff --git a/android/src/toga_android/libs/utilfile.py b/android/src/toga_android/libs/utilfile.py new file mode 100644 index 0000000000..156044fcb8 --- /dev/null +++ b/android/src/toga_android/libs/utilfile.py @@ -0,0 +1,205 @@ +import abc +import asyncio as aio +import io +import os +from dataclasses import dataclass + +from java.io import BufferedReader, InputStreamReader, OutputStreamWriter +from java.util import Objects + +import toga + + +class BaseFile(io.TextIOBase): + def __init__(self, stream, binary, encoding, newLineInt): + self.aloop: aio.AbstractEventLoop = toga.App.app._impl.loop + self._stream = stream + self._buffer = None + self._is_binary = binary + self._new_line_int = newLineInt + self._encoding = encoding + + def check_open(self): + """The defense mechanism on the open is that path""" + if self._buffer is None: + raise TypeError("File not open!") + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + def close(self): + self._stream.close() + if self._buffer is not None: + self._buffer.close() + + +@dataclass +class BaseDataOpen: + binary: bool + encoding: str + newLineInt: int + + +class BasePath(os.PathLike): + modes = [] + + def __init__(self, content, uri): + self._uri = uri + self._content = content + self._stream = None + + @abc.abstractmethod + def open(self, mode, encoding="utf-8", new_line="\n"): + """Method for opening a file by path""" + _new_line_int = new_line.encode(encoding)[0] + if mode not in self.modes: + raise ValueError( + f"""invalid mode {mode}. + It is allowed to use the following modes: R or RB""" + ) + _is_binary = "b" in mode + return BaseDataOpen(_is_binary, encoding, _new_line_int) + + +class BaseFileReader(BaseFile, abc.ABC): + + def __init__(self, stream, binary, encoding, newLineInt): + super().__init__(stream, binary, encoding, newLineInt) + input_stream_reader = InputStreamReader(Objects.requireNonNull(self._stream)) + self._buffer = BufferedReader(input_stream_reader) + + @abc.abstractmethod + def read(self, size=0): + pass + + @abc.abstractmethod + def readline(self, size=0): + pass + + @abc.abstractmethod + async def aread(self, size=0): + pass + + @abc.abstractmethod + async def areadline(self, size=0): + pass + + +class FileReader(BaseFileReader): + """A phalloid object for reading. + Reads the contents of the Android external storage file""" + + def readline(self, size: int = 0) -> str | bytes: + """A function for reading lines from a file + :param size: the number of rows to be counted. + (Currently not implemented, added for future implementation) + :return: Data type str[bytes] + (Depends on whether the flag and was passed) or the list of str[bytes] + """ + self.check_open() + res = bytearray() + counter = size if size else "-1" + while counter: + resp: int = self._buffer.read() + if resp == -1: + break + if resp == self._new_line_int: + break + res.append(resp) + if isinstance(counter, int): + counter -= 1 + if self._is_binary: + return bytes(res) + return res.decode(self._encoding) + + def read(self, size: int = 0) -> bytes | str: + """A function for reading a string + :param size: the number of characters to be counted. + If it is equal to 0, the entire file will be read + :return: Data type str[bytes] + (depends on whether the 'b' flag was passed)""" + self.check_open() + res = bytearray() + counter = size if size else "-1" + while counter: + resp: int = self._buffer.read() + if resp == -1: + break + res.append(resp) + if isinstance(counter, int): + counter -= 1 + if self._is_binary: + return bytes(res) + return res.decode(self._encoding) + + async def aread(self, size=0): + """Asynchronous function for reading a string + :parameter size: the number of characters to count. + If it is equal to 0, the entire file will be read + :returns: data type str[bytes] + (depends on whether the 'b' flag was passed)""" + self.check_open() + return await self.aloop.run_in_executor(None, self.read, size) + + async def areadline(self, size=0): + """Asynchronous function for reading lines from a file + : parameter size: the number of rows to count. + (Currently not implemented, added for future implementation) + :returns: data type str[byte] + (Depends on whether and flag was passed) or the str[byte] list""" + return await self.aloop.run_in_executor(None, self.readline, size) + + +class BaseFileWriter(BaseFile, abc.ABC): + + def __init__(self, stream, binary, encoding, newLineInt): + super().__init__(stream, binary, encoding, newLineInt) + self._buffer = OutputStreamWriter(self._stream) + + def _convertion_type_data(self, data): + if isinstance(data, bytes) and not self._is_binary: + return data.decode(self._encoding) + if isinstance(data, str) and self._is_binary: + return data.encode(self._encoding) + return data + + @abc.abstractmethod + def write(self, text: str | bytes): + pass + + @abc.abstractmethod + async def awrite(self, text): + pass + + +class FileWriter(BaseFileWriter): + def write(self, text: str | bytes): + data = self._convertion_type_data(text) + self._buffer.write(data) + + def awrite(self, text): + pass + + +class Path(BasePath): + modes = ["r", "rb", "w", "wb"] + + def open(self, mode="r", encoding="utf-8", new_line="\n"): + """A method for opening a file for reading along the path""" + dataOpen = super().open(mode.lower(), encoding, new_line) + if "r" in mode.lower(): + self._stream = self._content.openInputStream(self._uri) + return FileReader( + self._stream, dataOpen.binary, dataOpen.encoding, dataOpen.newLineInt + ) + if "w" in mode.lower(): + self._stream = self._content.openOutputStream(self._uri) + return FileWriter( + self._stream, dataOpen.binary, dataOpen.encoding, dataOpen.newLineInt + ) + + def __fspath__(self): + return str(self._uri) diff --git a/changes/2910.feature.rst b/changes/2910.feature.rst new file mode 100644 index 0000000000..af638fb320 --- /dev/null +++ b/changes/2910.feature.rst @@ -0,0 +1,7 @@ +Android implementation of OpenFileDialog + +Adding a utility module for reading from external storage files + +Adding singleton for file management on Android/Windows for API communication + +Support with Python 3.10+