Fix android detection when python4android is present (#277)

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>
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 @@
 
 
 @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
+    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 = 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
+            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 @@
                 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 @@
     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 @@
     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