Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix android detection when python4android is present #277

Merged
merged 10 commits into from
May 15, 2024
47 changes: 37 additions & 10 deletions src/platformdirs/android.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -117,23 +117,50 @@ 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):
result = path.split("/files")[0]
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


Expand Down
34 changes: 32 additions & 2 deletions tests/test_android.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down