android.py 10 KB

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