Source code for platformdirs.android

"""Android."""

from __future__ import annotations

import os
import re
import sys
from contextlib import suppress
from functools import cache, lru_cache
from typing import Final

from .api import PlatformDirsABC


[docs] class Android(PlatformDirsABC): # ruff:ignore[too-many-public-methods] """Platform directories for Android. Follows the guidance `from here <https://android.stackexchange.com/a/216132>`_. Directories are typically located under the app's private storage (``/data/user/<userid>/<packagename>/``). Makes use of the `appname <platformdirs.api.PlatformDirsABC.appname>`, `version <platformdirs.api.PlatformDirsABC.version>`, `opinion <platformdirs.api.PlatformDirsABC.opinion>`, `ensure_exists <platformdirs.api.PlatformDirsABC.ensure_exists>`. """ @property def user_data_dir(self) -> str: """Data directory tied to the user, e.g. ``/data/user/<userid>/<packagename>/files/<AppName>``.""" return self._append_app_name_and_version(_require_android_folder(), "files", private=True) @property def site_data_dir(self) -> str: """Data directory shared by users, same as `user_data_dir`.""" return self.user_data_dir @property def user_config_dir(self) -> str: """Config directory tied to the user, e.g. ``/data/user/<userid>/<packagename>/shared_prefs/<AppName>``.""" return self._append_app_name_and_version(_require_android_folder(), "shared_prefs", private=True) @property def site_config_dir(self) -> str: """Config directory shared by users, same as `user_config_dir`.""" return self.user_config_dir @property def user_cache_dir(self) -> str: """Cache directory tied to the user, e.g.,``/data/user/<userid>/<packagename>/cache/<AppName>``.""" return self._append_app_name_and_version(_require_android_folder(), "cache", private=True) @property def site_cache_dir(self) -> str: """Cache directory shared by users, same as `user_cache_dir`.""" return self.user_cache_dir @property def user_state_dir(self) -> str: """State directory tied to the user, same as `user_data_dir`.""" return self.user_data_dir @property def site_state_dir(self) -> str: """State directory shared by users, same as `user_state_dir`.""" return self.user_state_dir @property def user_log_dir(self) -> str: """Log directory tied to the user, same as `user_cache_dir` if not opinionated else ``log`` in it, e.g. ``/data/user/<userid>/<packagename>/cache/<AppName>/log``.""" path = self.user_cache_dir if self.opinion: path = os.path.join(path, "log") # ruff:ignore[os-path-join] self._optionally_create_directory(path, private=True) return path @property def site_log_dir(self) -> str: """Log directory shared by users, same as `user_log_dir`.""" return self.user_log_dir @property def user_documents_dir(self) -> str: """Documents directory tied to the user e.g. ``/storage/emulated/0/Documents``.""" return _android_public_folder("Documents") @property def user_downloads_dir(self) -> str: """Downloads directory tied to the user e.g. ``/storage/emulated/0/Download``.""" return _android_public_folder("Download") @property def user_pictures_dir(self) -> str: """Pictures directory tied to the user e.g. ``/storage/emulated/0/Pictures``.""" return _android_public_folder("Pictures") @property def user_videos_dir(self) -> str: """Videos directory tied to the user e.g. ``/storage/emulated/0/Movies``.""" return _android_public_folder("Movies") @property def user_music_dir(self) -> str: """Music directory tied to the user e.g. ``/storage/emulated/0/Music``.""" return _android_public_folder("Music") # Android lets apps create top-level shared folders only from its standard list, so these live in Documents. @property def user_desktop_dir(self) -> str: """Desktop directory tied to the user e.g. ``/storage/emulated/0/Documents/Desktop``.""" return f"{_shared_storage()}/Documents/Desktop" @property def user_projects_dir(self) -> str: """Projects directory tied to the user e.g. ``/storage/emulated/0/Documents/Projects``.""" return f"{_shared_storage()}/Documents/Projects" @property def user_publicshare_dir(self) -> str: """Public share directory tied to the user e.g. ``/storage/emulated/0/Documents/Public``.""" return f"{_shared_storage()}/Documents/Public" @property def user_templates_dir(self) -> str: """Templates directory tied to the user e.g. ``/storage/emulated/0/Documents/Templates``.""" return f"{_shared_storage()}/Documents/Templates" @property def user_fonts_dir(self) -> str: """Fonts directory tied to the user e.g. ``/storage/emulated/0/Documents/fonts``.""" return f"{_shared_storage()}/Documents/fonts" @property def user_preference_dir(self) -> str: """Preference directory tied to the user, same as ``user_config_dir``.""" return self.user_config_dir @property def user_bin_dir(self) -> str: """Bin directory tied to the user, e.g. ``/data/user/<userid>/<packagename>/files/bin``.""" return os.path.join(_require_android_folder(), "files", "bin") # ruff:ignore[os-path-join] @property def site_bin_dir(self) -> str: """Bin directory shared by users, same as `user_bin_dir`.""" return self.user_bin_dir @property def user_applications_dir(self) -> str: """Applications directory tied to the user, same as `user_data_dir`.""" return self.user_data_dir @property def site_applications_dir(self) -> str: """Applications directory shared by users, same as `user_applications_dir`.""" return self.user_applications_dir @property def user_runtime_dir(self) -> str: """Runtime directory tied to the user, same as `user_cache_dir` if not opinionated else ``tmp`` in it, e.g. ``/data/user/<userid>/<packagename>/cache/<AppName>/tmp``.""" path = self.user_cache_dir if self.opinion: path = os.path.join(path, "tmp") # ruff:ignore[os-path-join] self._optionally_create_directory(path, private=True) return path @property def site_runtime_dir(self) -> str: """Runtime directory shared by users, same as `user_runtime_dir`.""" return self.user_runtime_dir
def _require_android_folder() -> str: if (folder := _android_folder()) is None: msg = "cannot find the Android app folder: python4android and pyjnius failed and no app folder is on sys.path" raise RuntimeError(msg) return folder @lru_cache(maxsize=1) def _android_folder() -> str | None: """:returns: base folder for the Android OS or None if it cannot be found""" result = _python_for_android_folder() 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""" (?P<folder> /data/(?:data|user/\d+) # internal storage of the primary user or of user N /[^/]+ # package name, one path segment ) /files # the files directory the interpreter runs from """, re.VERBOSE, ) for path in sys.path: if match := pattern.match(path): result = match["folder"] 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""" (?P<folder> /mnt/expand/[a-fA-F0-9-]{36} # adopted storage volume, named by its UUID /(?:data|user/\d+) # the primary user or user N /[^/]+ # package name, one path segment ) /files # the files directory the interpreter runs from """, re.VERBOSE, ) for path in sys.path: if match := pattern.match(path): result = match["folder"] break else: result = None return result _USER_ID: Final = re.compile( r""" /user/ # the per-user data root, under /data or an adopted /mnt/expand volume (?P<user>\d+) # the Android user id / # followed by the package folder """, re.VERBOSE, ) def _shared_storage() -> str: # Android mounts each user's shared storage at /storage/emulated/<user id>, and an app sees only its own user's. user = match["user"] if (match := _USER_ID.search(_android_folder() or "")) else "0" return f"/storage/emulated/{user}" def _python_for_android_folder() -> str | None: with suppress(ImportError): from jnius import JavaException, autoclass # ruff:ignore[import-outside-top-level] # ty: ignore[unresolved-import] with suppress(JavaException): # A python-for-android service runs in its own process, where the activity is null. if (context := autoclass("org.kivy.android.PythonActivity").mActivity) is None: context = autoclass("org.kivy.android.PythonService").mService if context is not None: return context.getFilesDir().getParentFile().getAbsolutePath() return None @cache def _android_public_folder(name: str) -> str: with suppress(ImportError): from jnius import JavaException, autoclass # ruff:ignore[import-outside-top-level] # ty: ignore[unresolved-import] with suppress(JavaException): return autoclass("android.os.Environment").getExternalStoragePublicDirectory(name).getAbsolutePath() return f"{_shared_storage()}/{name}" __all__ = [ "Android", ]