Skip to content

Commit

Permalink
Fix android detection when python4android is present (#277)
Browse files Browse the repository at this point in the history
Co-authored-by: Bernát Gábor <gaborjbernat@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people committed May 15, 2024
1 parent dbf360f commit 5ec69d8
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 12 deletions.
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

0 comments on commit 5ec69d8

Please sign in to comment.