android.py 8.8 KB

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