android.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. """Android."""
  2. from __future__ import annotations
  3. import os
  4. import re
  5. import sys
  6. from functools import lru_cache
  7. from typing import TYPE_CHECKING, cast
  8. from .api import PlatformDirsABC
  9. class Android(PlatformDirsABC): # noqa: PLR0904
  10. """Platform directories for Android.
  11. Follows the guidance `from here <https://android.stackexchange.com/a/216132>`_. Directories are typically located
  12. under the app's private storage (``/data/user/<userid>/<packagename>/``).
  13. Makes use of the `appname <platformdirs.api.PlatformDirsABC.appname>`, `version
  14. <platformdirs.api.PlatformDirsABC.version>`, `opinion <platformdirs.api.PlatformDirsABC.opinion>`, `ensure_exists
  15. <platformdirs.api.PlatformDirsABC.ensure_exists>`.
  16. """
  17. @property
  18. def user_data_dir(self) -> str:
  19. """:returns: data directory tied to the user, e.g. ``/data/user/<userid>/<packagename>/files/<AppName>``"""
  20. return self._append_app_name_and_version(cast("str", _android_folder()), "files")
  21. @property
  22. def site_data_dir(self) -> str:
  23. """:returns: data directory shared by users, same as `user_data_dir`"""
  24. return self.user_data_dir
  25. @property
  26. def user_config_dir(self) -> str:
  27. """:returns: config directory tied to the user, e.g. ``/data/user/<userid>/<packagename>/shared_prefs/<AppName>``"""
  28. return self._append_app_name_and_version(cast("str", _android_folder()), "shared_prefs")
  29. @property
  30. def site_config_dir(self) -> str:
  31. """:returns: config directory shared by users, same as `user_config_dir`"""
  32. return self.user_config_dir
  33. @property
  34. def user_cache_dir(self) -> str:
  35. """:returns: cache directory tied to the user, e.g.,``/data/user/<userid>/<packagename>/cache/<AppName>``"""
  36. return self._append_app_name_and_version(cast("str", _android_folder()), "cache")
  37. @property
  38. def site_cache_dir(self) -> str:
  39. """:returns: cache directory shared by users, same as `user_cache_dir`"""
  40. return self.user_cache_dir
  41. @property
  42. def user_state_dir(self) -> str:
  43. """:returns: state directory tied to the user, same as `user_data_dir`"""
  44. return self.user_data_dir
  45. @property
  46. def site_state_dir(self) -> str:
  47. """:returns: state directory shared by users, same as `user_state_dir`"""
  48. return self.user_state_dir
  49. @property
  50. def user_log_dir(self) -> str:
  51. """:returns: 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``"""
  52. path = self.user_cache_dir
  53. if self.opinion:
  54. path = os.path.join(path, "log") # noqa: PTH118
  55. self._optionally_create_directory(path)
  56. return path
  57. @property
  58. def site_log_dir(self) -> str:
  59. """:returns: log directory shared by users, same as `user_log_dir`"""
  60. return self.user_log_dir
  61. @property
  62. def user_documents_dir(self) -> str:
  63. """:returns: documents directory tied to the user e.g. ``/storage/emulated/0/Documents``"""
  64. return _android_documents_folder()
  65. @property
  66. def user_downloads_dir(self) -> str:
  67. """:returns: downloads directory tied to the user e.g. ``/storage/emulated/0/Downloads``"""
  68. return _android_downloads_folder()
  69. @property
  70. def user_pictures_dir(self) -> str:
  71. """:returns: pictures directory tied to the user e.g. ``/storage/emulated/0/Pictures``"""
  72. return _android_pictures_folder()
  73. @property
  74. def user_videos_dir(self) -> str:
  75. """:returns: videos directory tied to the user e.g. ``/storage/emulated/0/DCIM/Camera``"""
  76. return _android_videos_folder()
  77. @property
  78. def user_music_dir(self) -> str:
  79. """:returns: music directory tied to the user e.g. ``/storage/emulated/0/Music``"""
  80. return _android_music_folder()
  81. @property
  82. def user_desktop_dir(self) -> str:
  83. """:returns: desktop directory tied to the user e.g. ``/storage/emulated/0/Desktop``"""
  84. return "/storage/emulated/0/Desktop"
  85. @property
  86. def user_bin_dir(self) -> str:
  87. """:returns: bin directory tied to the user, e.g. ``/data/user/<userid>/<packagename>/files/bin``"""
  88. return os.path.join(cast("str", _android_folder()), "files", "bin") # noqa: PTH118
  89. @property
  90. def site_bin_dir(self) -> str:
  91. """:returns: bin directory shared by users, same as `user_bin_dir`"""
  92. return self.user_bin_dir
  93. @property
  94. def user_applications_dir(self) -> str:
  95. """:returns: applications directory tied to the user, same as `user_data_dir`"""
  96. return self.user_data_dir
  97. @property
  98. def site_applications_dir(self) -> str:
  99. """:returns: applications directory shared by users, same as `user_applications_dir`"""
  100. return self.user_applications_dir
  101. @property
  102. def user_runtime_dir(self) -> str:
  103. """:returns: 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``"""
  104. path = self.user_cache_dir
  105. if self.opinion:
  106. path = os.path.join(path, "tmp") # noqa: PTH118
  107. self._optionally_create_directory(path)
  108. return path
  109. @property
  110. def site_runtime_dir(self) -> str:
  111. """:returns: runtime directory shared by users, same as `user_runtime_dir`"""
  112. return self.user_runtime_dir
  113. @lru_cache(maxsize=1)
  114. def _android_folder() -> str | None: # noqa: C901
  115. """:returns: base folder for the Android OS or None if it cannot be found"""
  116. result: str | None = None
  117. # type checker isn't happy with our "import android", just don't do this when type checking see
  118. # https://stackoverflow.com/a/61394121
  119. if not TYPE_CHECKING:
  120. try:
  121. # First try to get a path to android app using python4android (if available)...
  122. from android import mActivity # noqa: PLC0415
  123. context = cast("android.content.Context", mActivity.getApplicationContext()) # noqa: F821
  124. result = context.getFilesDir().getParentFile().getAbsolutePath()
  125. except Exception: # noqa: BLE001
  126. result = None
  127. if result is None:
  128. try:
  129. # ...and fall back to using plain pyjnius, if python4android isn't available or doesn't deliver any useful
  130. # result...
  131. from jnius import autoclass # noqa: PLC0415 # ty: ignore[unresolved-import]
  132. context = autoclass("android.content.Context")
  133. result = context.getFilesDir().getParentFile().getAbsolutePath()
  134. except Exception: # noqa: BLE001
  135. result = None
  136. if result is None:
  137. # and if that fails, too, find an android folder looking at path on the sys.path
  138. # warning: only works for apps installed under /data, not adopted storage etc.
  139. pattern = re.compile(r"/data/(data|user/\d+)/(.+)/files")
  140. for path in sys.path:
  141. if pattern.match(path):
  142. result = path.split("/files")[0]
  143. break
  144. else:
  145. result = None
  146. if result is None:
  147. # one last try: find an android folder looking at path on the sys.path taking adopted storage paths into
  148. # account
  149. pattern = re.compile(r"/mnt/expand/[a-fA-F0-9-]{36}/(data|user/\d+)/(.+)/files")
  150. for path in sys.path:
  151. if pattern.match(path):
  152. result = path.split("/files")[0]
  153. break
  154. else:
  155. result = None
  156. return result
  157. @lru_cache(maxsize=1)
  158. def _android_documents_folder() -> str:
  159. """:returns: documents folder for the Android OS"""
  160. # Get directories with pyjnius
  161. try:
  162. from jnius import autoclass # noqa: PLC0415 # ty: ignore[unresolved-import]
  163. context = autoclass("android.content.Context")
  164. environment = autoclass("android.os.Environment")
  165. documents_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DOCUMENTS).getAbsolutePath()
  166. except Exception: # noqa: BLE001
  167. documents_dir = "/storage/emulated/0/Documents"
  168. return documents_dir
  169. @lru_cache(maxsize=1)
  170. def _android_downloads_folder() -> str:
  171. """:returns: downloads folder for the Android OS"""
  172. # Get directories with pyjnius
  173. try:
  174. from jnius import autoclass # noqa: PLC0415 # ty: ignore[unresolved-import]
  175. context = autoclass("android.content.Context")
  176. environment = autoclass("android.os.Environment")
  177. downloads_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DOWNLOADS).getAbsolutePath()
  178. except Exception: # noqa: BLE001
  179. downloads_dir = "/storage/emulated/0/Downloads"
  180. return downloads_dir
  181. @lru_cache(maxsize=1)
  182. def _android_pictures_folder() -> str:
  183. """:returns: pictures folder for the Android OS"""
  184. # Get directories with pyjnius
  185. try:
  186. from jnius import autoclass # noqa: PLC0415 # ty: ignore[unresolved-import]
  187. context = autoclass("android.content.Context")
  188. environment = autoclass("android.os.Environment")
  189. pictures_dir: str = context.getExternalFilesDir(environment.DIRECTORY_PICTURES).getAbsolutePath()
  190. except Exception: # noqa: BLE001
  191. pictures_dir = "/storage/emulated/0/Pictures"
  192. return pictures_dir
  193. @lru_cache(maxsize=1)
  194. def _android_videos_folder() -> str:
  195. """:returns: videos folder for the Android OS"""
  196. # Get directories with pyjnius
  197. try:
  198. from jnius import autoclass # noqa: PLC0415 # ty: ignore[unresolved-import]
  199. context = autoclass("android.content.Context")
  200. environment = autoclass("android.os.Environment")
  201. videos_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DCIM).getAbsolutePath()
  202. except Exception: # noqa: BLE001
  203. videos_dir = "/storage/emulated/0/DCIM/Camera"
  204. return videos_dir
  205. @lru_cache(maxsize=1)
  206. def _android_music_folder() -> str:
  207. """:returns: music folder for the Android OS"""
  208. # Get directories with pyjnius
  209. try:
  210. from jnius import autoclass # noqa: PLC0415 # ty: ignore[unresolved-import]
  211. context = autoclass("android.content.Context")
  212. environment = autoclass("android.os.Environment")
  213. music_dir: str = context.getExternalFilesDir(environment.DIRECTORY_MUSIC).getAbsolutePath()
  214. except Exception: # noqa: BLE001
  215. music_dir = "/storage/emulated/0/Music"
  216. return music_dir
  217. __all__ = [
  218. "Android",
  219. ]