diff --git a/src/platformdirs/android.py b/src/platformdirs/android.py index fefafd3..afd3141 100644 --- a/src/platformdirs/android.py +++ b/src/platformdirs/android.py @@ -6,7 +6,7 @@ import re import sys from functools import lru_cache -from typing import cast +from typing import TYPE_CHECKING, cast from .api import PlatformDirsABC @@ -117,16 +117,33 @@ def site_runtime_dir(self) -> str: @lru_cache(maxsize=1) -def _android_folder() -> str | None: +def _android_folder() -> str | None: # noqa: C901, PLR0912 """:return: base folder for the Android OS or None if it cannot be found""" - try: - # First try to get a path to android app via pyjnius - from jnius import autoclass # noqa: PLC0415 - - context = autoclass("android.content.Context") - result: str | None = context.getFilesDir().getParentFile().getAbsolutePath() - except Exception: # noqa: BLE001 - # if fails find an android folder looking a path on the sys.path + result: str | None = None + # type checker isn't happy with our "import android", just don't do this when type checking see + # https://stackoverflow.com/a/61394121 + if not TYPE_CHECKING: + try: + # First try to get a path to android app using python4android (if available)... + from android import mActivity # noqa: PLC0415 + + context = cast("android.content.Context", mActivity.getApplicationContext()) # noqa: F821 + result = context.getFilesDir().getParentFile().getAbsolutePath() + except Exception: # noqa: BLE001 + result = None + if result is None: + try: + # ...and fall back to using plain pyjnius, if python4android isn't available or doesn't deliver any useful + # result... + from jnius import autoclass # noqa: PLC0415 + + context = autoclass("android.content.Context") + result = context.getFilesDir().getParentFile().getAbsolutePath() + except Exception: # noqa: BLE001 + result = None + if result is None: + # and if that fails, too, find an android folder looking at path on the sys.path + # warning: only works for apps installed under /data, not adopted storage etc. pattern = re.compile(r"/data/(data|user/\d+)/(.+)/files") for path in sys.path: if pattern.match(path): @@ -134,6 +151,16 @@ def _android_folder() -> str | None: break else: result = None + if result is None: + # one last try: find an android folder looking at path on the sys.path taking adopted storage paths into + # account + pattern = re.compile(r"/mnt/expand/[a-fA-F0-9-]{36}/(data|user/\d+)/(.+)/files") + for path in sys.path: + if pattern.match(path): + result = path.split("/files")[0] + break + else: + result = None return result diff --git a/tests/test_android.py b/tests/test_android.py index 57ac1b5..7166ee4 100644 --- a/tests/test_android.py +++ b/tests/test_android.py @@ -67,10 +67,13 @@ def test_android(mocker: MockerFixture, params: dict[str, Any], func: str) -> No assert result == expected -def test_android_folder_from_jnius(mocker: MockerFixture) -> None: +def test_android_folder_from_jnius(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch) -> None: from platformdirs import PlatformDirs # noqa: PLC0415 from platformdirs.android import _android_folder # noqa: PLC0415 + mocker.patch.dict(sys.modules, {"android": MagicMock(side_effect=ModuleNotFoundError)}) + monkeypatch.delitem(__import__("sys").modules, "android") + _android_folder.cache_clear() if PlatformDirs is Android: @@ -93,15 +96,42 @@ def test_android_folder_from_jnius(mocker: MockerFixture) -> None: assert autoclass.call_count == 1 +def test_android_folder_from_p4a(mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch) -> None: + from platformdirs.android import _android_folder # noqa: PLC0415 + + mocker.patch.dict(sys.modules, {"jnius": MagicMock(side_effect=ModuleNotFoundError)}) + monkeypatch.delitem(__import__("sys").modules, "jnius") + + _android_folder.cache_clear() + + get_absolute_path = MagicMock(return_value="/A") + get_parent_file = MagicMock(getAbsolutePath=get_absolute_path) + get_files_dir = MagicMock(getParentFile=MagicMock(return_value=get_parent_file)) + get_application_context = MagicMock(getFilesDir=MagicMock(return_value=get_files_dir)) + m_activity = MagicMock(getApplicationContext=MagicMock(return_value=get_application_context)) + mocker.patch.dict(sys.modules, {"android": MagicMock(mActivity=m_activity)}) + + result = _android_folder() + assert result == "/A" + assert get_absolute_path.call_count == 1 + + assert _android_folder() is result + assert get_absolute_path.call_count == 1 + + @pytest.mark.parametrize( "path", [ "/data/user/1/a/files", "/data/data/a/files", + "/mnt/expand/8e06fc2f-a86a-44e8-81ce-109e0eedd5ed/user/1/a/files", ], ) def test_android_folder_from_sys_path(mocker: MockerFixture, path: str, monkeypatch: pytest.MonkeyPatch) -> None: - mocker.patch.dict(sys.modules, {"jnius": MagicMock(autoclass=MagicMock(side_effect=ModuleNotFoundError))}) + mocker.patch.dict(sys.modules, {"jnius": MagicMock(side_effect=ModuleNotFoundError)}) + monkeypatch.delitem(__import__("sys").modules, "jnius") + mocker.patch.dict(sys.modules, {"android": MagicMock(side_effect=ModuleNotFoundError)}) + monkeypatch.delitem(__import__("sys").modules, "android") from platformdirs.android import _android_folder # noqa: PLC0415