From 7d5fdee64b1280469edc2020b3084ed8f9421162 Mon Sep 17 00:00:00 2001 From: Jonas Thiem Date: Thu, 17 Jan 2019 22:27:04 +0100 Subject: [PATCH 1/2] Add functions for obtaining the default storage paths --- doc/source/apis.rst | 54 +++++++++ .../recipes/android/src/android/storage.py | 103 ++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 pythonforandroid/recipes/android/src/android/storage.py diff --git a/doc/source/apis.rst b/doc/source/apis.rst index 7c3c307f4e..beae347625 100644 --- a/doc/source/apis.rst +++ b/doc/source/apis.rst @@ -5,6 +5,60 @@ Working on Android This page gives details on accessing Android APIs and managing other interactions on Android. +Storage paths +------------- + +If you want to store and retrieve data, you shouldn't just save to +the current directory, and not hardcode `/sdcard/` or some other +path either - it might differ per device. + +Instead, the `android` module which you can add to your `--requirements` +allows you to query the most commonly required paths:: + + from android.storage import app_storage_path + settings_path = app_storage_path() + + from android.storage import primary_external_storage_path + primary_ext_storage = primary_external_storage_path() + + from android.storage import secondary_external_storage_path + secondary_ext_storage = secondary_external_storage_path() + +`app_storage_path()` gives you Android's so-called "internal storage" +which is specific to your app and cannot seen by others or the user. +It compares best to the AppData directory on Windows. + +`primary_external_storage_path()` returns Android's so-called +"primary external storage", often found at `/sdcard/` and potentially +accessible to any other app. +It compares best to the Documents directory on Windows. +Requires `Permission.WRITE_EXTERNAL_STORAGE` to read and write to. + +`secondary_external_storage_path()` returns Android's so-called +"secondary external storage", often found at `/storage/External_SD/`. +It compares best to an external disk plugged to a Desktop PC, and can +after a device restart become inaccessible if removed. +Requires `Permission.WRITE_EXTERNAL_STORAGE` to read and write to. + +.. warning:: + Even if `secondary_external_storage_path` returns a path + the external sd card may still not be present. + Only non-empty contents or a successful write indicate that it is. + +Read more on all the different storage types and what to use them for +in the Android documentation: + +https://developer.android.com/training/data-storage/files + +A note on permissions +~~~~~~~~~~~~~~~~~~~~~ + +Only the internal storage is always accessible with no additional +permissions. For both primary and secondary external storage, you need +to obtain `Permission.WRITE_EXTERNAL_STORAGE` **and the user may deny it.** +Also, if you get it, both forms of external storage may only allow +your app to write to the common pre-existing folders like "Music", +"Documents", and so on. (see the Android Docs linked above for details) Runtime permissions ------------------- diff --git a/pythonforandroid/recipes/android/src/android/storage.py b/pythonforandroid/recipes/android/src/android/storage.py new file mode 100644 index 0000000000..d1b5cde472 --- /dev/null +++ b/pythonforandroid/recipes/android/src/android/storage.py @@ -0,0 +1,103 @@ + +from jnius import autoclass, cast +import os + + +Environment = autoclass('android.os.Environment') + + +def _android_has_is_removable_func(): + VERSION = autoclass('android.os.Build$VERSION') + return (VERSION.SDK_INT >= 24) + + +def _get_sdcard_path(): + """ Internal function to return getExternalStorageDirectory() + path. This is internal because it may either return the internal, + or an external sd card, depending on the device. + Use primary_external_storage_path() + or secondary_external_storage_path() instead which try to + distinguish this properly. + """ + return ( + Environment.getExternalStorageDirectory().getAbsolutePath() + ) + + +def app_storage_path(): + """ Locate the built-in device storage used for this app only. + + This storage is APP-SPECIFIC, and not visible to other apps. + It will be wiped when your app is uninstalled. + + Returns directory path to storage. + """ + PythonActivity = autoclass('org.kivy.android.PythonActivity') + currentActivity = cast('android.app.Activity', + PythonActivity.mActivity) + context = cast('android.content.ContextWrapper', + currentActivity.getApplicationContext()) + file_p = cast('java.io.File', context.getFilesDir()) + return os.path.normpath(os.path.abspath( + file_p.getAbsolutePath().replace("/", os.path.sep))) + + +def primary_external_storage_path(): + """ Locate the built-in device storage that user can see via file browser. + Often found at: /sdcard/ + + This is storage is SHARED, and visible to other apps and the user. + It will remain untouched when your app is uninstalled. + + Returns directory path to storage. + + WARNING: You need storage permissions to access this storage. + """ + if _android_has_is_removable_func(): + sdpath = _get_sdcard_path() + # Apparently this can both return primary (built-in) or + # secondary (removable) external storage depending on the device, + # therefore check that we got what we wanted: + if not Environment.isExternalStorageRemovable(sdpath): + return sdpath + if "EXTERNAL_STORAGE" in os.environ: + return os.environ["EXTERNAL_STORAGE"] + raise RuntimeError( + "unexpectedly failed to determine " + + "primary external storage path" + ) + + +def secondary_external_storage_path(): + """ Locate the external SD Card storage, which may not be present. + Often found at: /sdcard/External_SD/ + + This storage is SHARED, visible to other apps, and may not be + be available if the user didn't put in an external SD card. + It will remain untouched when your app is uninstalled. + + Returns None if not found, otherwise path to storage. + + WARNING: You need storage permissions to access this storage. + If it is not writable and presents as empty even with + permissions, then the external sd card may not be present. + """ + if _android_has_is_removable_func: + # See if getExternalStorageDirectory() returns secondary ext storage: + sdpath = _get_sdcard_path() + # Apparently this can both return primary (built-in) or + # secondary (removable) external storage depending on the device, + # therefore check that we got what we wanted: + if Environment.isExternalStorageRemovable(sdpath): + if os.path.exists(sdpath): + return sdpath + + # See if we can take a guess based on environment variables: + p = None + if "SECONDARY_STORAGE" in os.environ: + p = os.environ["SECONDARY_STORAGE"] + elif "EXTERNAL_SDCARD_STORAGE" in os.environ: + p = os.environ["EXTERNAL_SDCARD_STORAGE"] + if os.path.exists(p): + return p + return None From c63588dd1ec08870b59be9cf6d7bec70e17b9af9 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 28 Jul 2019 23:08:29 +0200 Subject: [PATCH 2/2] Update storage.py fixes runtime errors 1) fixes isExternalStorageRemovable is expecting a file object, error was: ``` 07-28 16:36:46.845 2152 2784 I python : File "/home/andre/workspace/EtherollApp/.buildozer/android/platform/build/build/python-installs/etheroll/android/storage.py", line 61, in primary_external_storage_path 07-28 16:36:46.847 2152 2784 I python : File "jnius/jnius_export_class.pxi", line 1034, in jnius.jnius.JavaMultipleMethod.__call__ 07-28 16:36:46.848 2152 2784 I python : jnius.jnius.JavaException: No methods matching your arguments, available: ['()Z', '(Ljava/io/File;)Z'] ``` 2) fixes `os.path.exists()` doesn't accept `None`, error was: 07-28 22:31:25.877 31203 31313 I python : File "/home/andre/workspace/EtherollApp/.buildozer/android/platform/build/build/python-installs/etheroll/android/storage.py", line 101, in secondary_external_storage_path 07-28 22:31:25.878 31203 31313 I python : File "/home/andre/workspace/EtherollApp/.buildozer/android/platform/build/build/other_builds/python3-libffi-openssl-sqlite3/armeabi-v7a__ndk_target_21/python3/Lib/genericpath.py", line 19, in exists 07-28 22:31:25.879 31203 31313 I python : TypeError: stat: path should be string, bytes, os.PathLike or integer, not NoneType 3) fixes on services `PythonActivity.mActivity` is None, refs https://github.com/kivy/kivy/pull/6388 --- .../recipes/android/src/android/storage.py | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/pythonforandroid/recipes/android/src/android/storage.py b/pythonforandroid/recipes/android/src/android/storage.py index d1b5cde472..f4d01403bc 100644 --- a/pythonforandroid/recipes/android/src/android/storage.py +++ b/pythonforandroid/recipes/android/src/android/storage.py @@ -1,9 +1,9 @@ - from jnius import autoclass, cast import os Environment = autoclass('android.os.Environment') +File = autoclass('java.io.File') def _android_has_is_removable_func(): @@ -24,6 +24,19 @@ def _get_sdcard_path(): ) +def _get_activity(): + """ + Retrieves the activity from `PythonActivity` fallback to `PythonService`. + """ + PythonActivity = autoclass('org.kivy.android.PythonActivity') + activity = PythonActivity.mActivity + if activity is None: + # assume we're running from the background service + PythonService = autoclass('org.kivy.android.PythonService') + activity = PythonService.mService + return activity + + def app_storage_path(): """ Locate the built-in device storage used for this app only. @@ -32,9 +45,8 @@ def app_storage_path(): Returns directory path to storage. """ - PythonActivity = autoclass('org.kivy.android.PythonActivity') - currentActivity = cast('android.app.Activity', - PythonActivity.mActivity) + activity = _get_activity() + currentActivity = cast('android.app.Activity', activity) context = cast('android.content.ContextWrapper', currentActivity.getApplicationContext()) file_p = cast('java.io.File', context.getFilesDir()) @@ -58,7 +70,7 @@ def primary_external_storage_path(): # Apparently this can both return primary (built-in) or # secondary (removable) external storage depending on the device, # therefore check that we got what we wanted: - if not Environment.isExternalStorageRemovable(sdpath): + if not Environment.isExternalStorageRemovable(File(sdpath)): return sdpath if "EXTERNAL_STORAGE" in os.environ: return os.environ["EXTERNAL_STORAGE"] @@ -88,7 +100,7 @@ def secondary_external_storage_path(): # Apparently this can both return primary (built-in) or # secondary (removable) external storage depending on the device, # therefore check that we got what we wanted: - if Environment.isExternalStorageRemovable(sdpath): + if Environment.isExternalStorageRemovable(File(sdpath)): if os.path.exists(sdpath): return sdpath @@ -98,6 +110,6 @@ def secondary_external_storage_path(): p = os.environ["SECONDARY_STORAGE"] elif "EXTERNAL_SDCARD_STORAGE" in os.environ: p = os.environ["EXTERNAL_SDCARD_STORAGE"] - if os.path.exists(p): + if p is not None and os.path.exists(p): return p return None