From 1c753e4a22642b009b6e7ef35e1280e2d1012675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 6 Jan 2021 16:37:04 +0100 Subject: [PATCH] Use hacky methods to retrieve SD card directory if available. This will disappear in future Android versions but can be useful for older devices. Fixes (or at least tries to) #10199, at least for some devices. Might help #13827 ? Tested on Pocophone F1. --- Common/System/System.h | 11 +- Qt/QtMain.cpp | 7 ++ SDL/SDLMain.cpp | 7 ++ UI/MainScreen.cpp | 15 ++- UWP/PPSSPP_UWPMain.cpp | 7 ++ Windows/main.cpp | 7 ++ android/jni/app-android.cpp | 43 ++++++-- .../src/org/ppsspp/ppsspp/NativeActivity.java | 103 +++++++++++++++++- android/src/org/ppsspp/ppsspp/NativeApp.java | 2 +- headless/Headless.cpp | 14 +-- ios/main.mm | 7 ++ libretro/libretro.cpp | 2 + unittest/UnitTest.cpp | 3 + 13 files changed, 202 insertions(+), 26 deletions(-) diff --git a/Common/System/System.h b/Common/System/System.h index 2d75b95ae92b..b304e9621ad9 100644 --- a/Common/System/System.h +++ b/Common/System/System.h @@ -40,6 +40,8 @@ void System_SendMessage(const char *command, const char *parameter); PermissionStatus System_GetPermissionStatus(SystemPermission permission); void System_AskForPermission(SystemPermission permission); +std::vector System_GetExternalStorageDirs(); + // This will get muddy with multi-screen support :/ But this will always be the type of the main device. enum SystemDeviceType { DEVICE_TYPE_MOBILE = 0, // phones and pads @@ -55,14 +57,16 @@ enum SystemProperty { SYSPROP_CLIPBOARD_TEXT, SYSPROP_GPUDRIVER_VERSION, + // Separate SD cards or similar. + // Need hacky solutions to get at this. + SYSPROP_HAS_ADDITIONAL_STORAGE, + SYSPROP_ADDITIONAL_STORAGE_DIRS, + SYSPROP_HAS_FILE_BROWSER, SYSPROP_HAS_FOLDER_BROWSER, SYSPROP_HAS_IMAGE_BROWSER, SYSPROP_HAS_BACK_BUTTON, - // Legacy on Android 29+. - SYSPROP_HAS_EXTERNAL_STORAGE, - // Available as Int: SYSPROP_SYSTEMVERSION, SYSPROP_DISPLAY_XRES, @@ -98,6 +102,7 @@ enum SystemProperty { }; std::string System_GetProperty(SystemProperty prop); +std::vector System_GetPropertyStringVec(SystemProperty prop); int System_GetPropertyInt(SystemProperty prop); float System_GetPropertyFloat(SystemProperty prop); bool System_GetPropertyBool(SystemProperty prop); diff --git a/Qt/QtMain.cpp b/Qt/QtMain.cpp index 4dfeaebeb565..32ae76e6c614 100644 --- a/Qt/QtMain.cpp +++ b/Qt/QtMain.cpp @@ -149,6 +149,13 @@ std::string System_GetProperty(SystemProperty prop) { } } +std::vector System_GetPropertyStringVec(SystemProperty prop) { + switch (prop) { + default: + return std::vector(); + } +} + int System_GetPropertyInt(SystemProperty prop) { switch (prop) { #if defined(SDL) diff --git a/SDL/SDLMain.cpp b/SDL/SDLMain.cpp index 8d43dfdb663d..c65f583fc6b9 100644 --- a/SDL/SDLMain.cpp +++ b/SDL/SDLMain.cpp @@ -325,6 +325,13 @@ std::string System_GetProperty(SystemProperty prop) { } } +std::vector System_GetPropertyStringVec(SystemProperty prop) { + switch (prop) { + default: + return std::vector(); + } +} + int System_GetPropertyInt(SystemProperty prop) { switch (prop) { case SYSPROP_AUDIO_SAMPLE_RATE: diff --git a/UI/MainScreen.cpp b/UI/MainScreen.cpp index 2ee21fbfa712..6857658a07b2 100644 --- a/UI/MainScreen.cpp +++ b/UI/MainScreen.cpp @@ -546,8 +546,17 @@ UI::EventReturn GameBrowser::BrowseClick(UI::EventParams &e) { } UI::EventReturn GameBrowser::StorageClick(UI::EventParams &e) { - // TODO: Get the SD card directory on Android. - SetPath(""); + std::vector storageDirs = System_GetPropertyStringVec(SYSPROP_ADDITIONAL_STORAGE_DIRS); + if (storageDirs.empty()) { + // Shouldn't happen - this button shouldn't be clickable. + return UI::EVENT_DONE; + } + if (storageDirs.size() == 1) { + SetPath(storageDirs[0]); + } else { + // TODO: We should popup a dialog letting the user choose one. + SetPath(storageDirs[0]); + } return UI::EVENT_DONE; } @@ -672,7 +681,7 @@ void GameBrowser::Refresh() { } else { topBar->Add(new Choice(mm->T("Home"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::HomeClick); } - if (System_GetPropertyBool(SYSPROP_HAS_EXTERNAL_STORAGE)) { + if (System_GetPropertyBool(SYSPROP_HAS_ADDITIONAL_STORAGE)) { topBar->Add(new Choice(ImageID("I_SDCARD"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::StorageClick); } topBar->Add(new Choice(ImageID("I_HOME"), new LayoutParams(WRAP_CONTENT, 64.0f)))->OnClick.Handle(this, &GameBrowser::HomeClick); diff --git a/UWP/PPSSPP_UWPMain.cpp b/UWP/PPSSPP_UWPMain.cpp index 4248c5fa2b33..60a73f86952a 100644 --- a/UWP/PPSSPP_UWPMain.cpp +++ b/UWP/PPSSPP_UWPMain.cpp @@ -356,6 +356,13 @@ std::string System_GetProperty(SystemProperty prop) { } } +std::vector System_GetPropertyStringVec(SystemProperty prop) { + switch (prop) { + default: + return std::vector(); + } +} + int System_GetPropertyInt(SystemProperty prop) { switch (prop) { case SYSPROP_AUDIO_SAMPLE_RATE: diff --git a/Windows/main.cpp b/Windows/main.cpp index 1153376021c2..c7e9dc9fd678 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -216,6 +216,13 @@ std::string System_GetProperty(SystemProperty prop) { } } +std::vector System_GetPropertyStringVec(SystemProperty prop) { + switch (prop) { + default: + return std::vector(); + } +} + // Ugly! extern WindowsAudioBackend *winAudioBackend; diff --git a/android/jni/app-android.cpp b/android/jni/app-android.cpp index 82a69e8c2712..38087968f005 100644 --- a/android/jni/app-android.cpp +++ b/android/jni/app-android.cpp @@ -13,6 +13,8 @@ #include #include +#include + #ifndef _MSC_VER #include #include @@ -122,6 +124,8 @@ std::string langRegion; std::string mogaVersion; std::string boardName; +std::vector g_additionalStorageDirs; + static float left_joystick_x_async; static float left_joystick_y_async; static float right_joystick_x_async; @@ -364,6 +368,15 @@ std::string System_GetProperty(SystemProperty prop) { } } +std::vector System_GetPropertyStringVec(SystemProperty prop) { + switch (prop) { + case SYSPROP_ADDITIONAL_STORAGE_DIRS: + return g_additionalStorageDirs; + default: + return std::vector(); + } +} + int System_GetPropertyInt(SystemProperty prop) { switch (prop) { case SYSPROP_SYSTEMVERSION: @@ -410,6 +423,8 @@ bool System_GetPropertyBool(SystemProperty prop) { return androidVersion >= 23; // 6.0 Marshmallow introduced run time permissions. case SYSPROP_SUPPORTS_SUSTAINED_PERF_MODE: return sustainedPerfSupported; // 7.0 introduced sustained performance mode as an optional feature. + case SYSPROP_HAS_ADDITIONAL_STORAGE: + return !g_additionalStorageDirs.empty(); case SYSPROP_HAS_BACK_BUTTON: return true; case SYSPROP_HAS_IMAGE_BROWSER: @@ -530,16 +545,19 @@ static void parse_args(std::vector &args, const std::string value) } } +// Need to use raw Android logging before NativeInit. +#define EARLY_LOG(...) __android_log_print(ANDROID_LOG_INFO, "PPSSPP", __VA_ARGS__) + extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init (JNIEnv *env, jclass, jstring jmodel, jint jdeviceType, jstring jlangRegion, jstring japkpath, - jstring jdataDir, jstring jexternalDir, jstring jlibraryDir, jstring jcacheDir, jstring jshortcutParam, + jstring jdataDir, jstring jexternalStorageDir, jstring jadditionalStorageDirs, jstring jlibraryDir, jstring jcacheDir, jstring jshortcutParam, jint jAndroidVersion, jstring jboard) { setCurrentThreadName("androidInit"); // Makes sure we get early permission grants. ProcessFrameCommands(env); - INFO_LOG(SYSTEM, "NativeApp.init() -- begin"); + EARLY_LOG("NativeApp.init() -- begin"); PROFILE_INIT(); renderer_inited = false; @@ -559,9 +577,18 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init systemName = GetJavaString(env, jmodel); langRegion = GetJavaString(env, jlangRegion); - INFO_LOG(SYSTEM, "NativeApp.init(): device name: '%s'", systemName.c_str()); + EARLY_LOG("NativeApp.init(): device name: '%s'", systemName.c_str()); + + std::string externalStorageDir = GetJavaString(env, jexternalStorageDir); + std::string additionalStorageDirsString = GetJavaString(env, jadditionalStorageDirs); + + if (!additionalStorageDirsString.empty()) { + SplitString(additionalStorageDirsString, ':', g_additionalStorageDirs); + for (auto &str : g_additionalStorageDirs) { + EARLY_LOG("Additional storage: %s", str.c_str()); + } + } - std::string externalDir = GetJavaString(env, jexternalDir); std::string user_data_path = GetJavaString(env, jdataDir); if (user_data_path.size() > 0) user_data_path += "/"; @@ -570,8 +597,8 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init std::string cacheDir = GetJavaString(env, jcacheDir); std::string buildBoard = GetJavaString(env, jboard); boardName = buildBoard; - INFO_LOG(SYSTEM, "NativeApp.init(): External storage path: %s", externalDir.c_str()); - INFO_LOG(SYSTEM, "NativeApp.init(): Launch shortcut parameter: %s", shortcut_param.c_str()); + EARLY_LOG("NativeApp.init(): External storage path: %s", externalStorageDir.c_str()); + EARLY_LOG("NativeApp.init(): Launch shortcut parameter: %s", shortcut_param.c_str()); std::string app_name; std::string app_nice_name; @@ -600,7 +627,9 @@ extern "C" void Java_org_ppsspp_ppsspp_NativeApp_init } } - NativeInit((int)args.size(), &args[0], user_data_path.c_str(), externalDir.c_str(), cacheDir.c_str()); + NativeInit((int)args.size(), &args[0], user_data_path.c_str(), externalStorageDir.c_str(), cacheDir.c_str()); + + // No need to use EARLY_LOG anymore. retry: // Now that we've loaded config, set javaGL. diff --git a/android/src/org/ppsspp/ppsspp/NativeActivity.java b/android/src/org/ppsspp/ppsspp/NativeActivity.java index 54ae0f04aa4b..87fa0abfbed9 100644 --- a/android/src/org/ppsspp/ppsspp/NativeActivity.java +++ b/android/src/org/ppsspp/ppsspp/NativeActivity.java @@ -46,6 +46,7 @@ import java.io.File; import java.lang.reflect.Field; import java.util.List; +import java.util.ArrayList; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -241,6 +242,91 @@ public void setShortcutParam(String shortcutParam) { this.shortcutParam = ((shortcutParam == null) ? "" : shortcutParam); } + // Unofficial hacks to get a list of SD cards that are not the main "external storage". + private static List getSdCardPaths(final Context context) { + // Q is the last version that will support normal file access. + List list = null; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { + Log.i(TAG, "getSdCardPaths: Trying KitKat method"); + list = getSdCardPaths19(context); + } + + if (list == null) { + Log.i(TAG, "getSdCardPaths: Attempting fallback"); + // Try another method. + String removableStoragePath; + list = new ArrayList(); + File fileList[] = new File("/storage/").listFiles(); + for (File file : fileList) { + if (!file.getAbsolutePath().equalsIgnoreCase(Environment.getExternalStorageDirectory().getAbsolutePath()) && file.isDirectory() && file.canRead()) { + list.add(file.getAbsolutePath()); + } + } + } + + // TODO: On older devices, try System.getenv(“EXTERNAL_SDCARD_STORAGE”) + + if (list == null) { + return new ArrayList(); + } else { + return list; + } + } + + /** + * returns a list of all available sd cards paths, or null if not found. + */ + @TargetApi(Build.VERSION_CODES.KITKAT) + private static List getSdCardPaths19(final Context context) + { + final File[] externalCacheDirs = context.getExternalCacheDirs(); + if (externalCacheDirs == null || externalCacheDirs.length==0) + return null; + if (externalCacheDirs.length == 1) { + if (externalCacheDirs[0] == null) + return null; + final String storageState = Environment.getStorageState(externalCacheDirs[0]); + if (!Environment.MEDIA_MOUNTED.equals(storageState)) + return null; + if (Environment.isExternalStorageEmulated()) + return null; + } + final List result = new ArrayList<>(); + if (externalCacheDirs.length == 1) + result.add(getRootOfInnerSdCardFolder(externalCacheDirs[0])); + for (int i = 1; i < externalCacheDirs.length; ++i) + { + final File file=externalCacheDirs[i]; + if (file == null) + continue; + final String storageState=Environment.getStorageState(file); + if (Environment.MEDIA_MOUNTED.equals(storageState)) + result.add(getRootOfInnerSdCardFolder(externalCacheDirs[i])); + } + if (result.isEmpty()) + return null; + return result; + } + + /** Given any file/folder inside an sd card, this will return the path of the sd card */ + private static String getRootOfInnerSdCardFolder(File file) + { + if (file == null) + return null; + final long totalSpace = file.getTotalSpace(); + while (true) { + final File parentFile = file.getParentFile(); + if (parentFile == null || !parentFile.canRead()) { + break; + } + if (parentFile.getTotalSpace() != totalSpace) { + break; + } + file = parentFile; + } + return file.getAbsolutePath(); + } + public void Initialize() { // Initialize audio classes. Do this here since detectOptimalAudioSettings() // needs audioManager @@ -292,9 +378,20 @@ public void Initialize() { isXperiaPlay = IsXperiaPlay(); String libraryDir = getApplicationLibraryDir(appInfo); - File sdcard = Environment.getExternalStorageDirectory(); - String externalStorageDir = sdcard.getAbsolutePath(); + String extStorageState = Environment.getExternalStorageState(); + String extStorageDir = Environment.getExternalStorageDirectory().getAbsolutePath(); + + Log.i(TAG, "Ext storage: " + extStorageState + " " + extStorageDir); + + List sdCards = getSdCardPaths(this); + for (String sdcard: sdCards) { + Log.i(TAG, "SD card: " + sdcard); + } + String additionalStorageDirs = String.join(":", sdCards); + + Log.i(TAG, "End of storage paths"); + File filesDir = this.getFilesDir(); String dataDir = null; if (filesDir != null) { @@ -310,7 +407,7 @@ public void Initialize() { overrideShortcutParam = null; NativeApp.audioConfig(optimalFramesPerBuffer, optimalSampleRate); - NativeApp.init(model, deviceType, languageRegion, apkFilePath, dataDir, externalStorageDir, libraryDir, cacheDir, shortcut, Build.VERSION.SDK_INT, Build.BOARD); + NativeApp.init(model, deviceType, languageRegion, apkFilePath, dataDir, extStorageDir, additionalStorageDirs, libraryDir, cacheDir, shortcut, Build.VERSION.SDK_INT, Build.BOARD); // Allow C++ to tell us to use JavaGL or not. javaGL = "true".equalsIgnoreCase(NativeApp.queryConfig("androidJavaGL")); diff --git a/android/src/org/ppsspp/ppsspp/NativeApp.java b/android/src/org/ppsspp/ppsspp/NativeApp.java index 632c625bed02..4a72d4c6a99a 100644 --- a/android/src/org/ppsspp/ppsspp/NativeApp.java +++ b/android/src/org/ppsspp/ppsspp/NativeApp.java @@ -12,7 +12,7 @@ public class NativeApp { public static final int DEVICE_TYPE_TV = 1; public static final int DEVICE_TYPE_DESKTOP = 2; - public static native void init(String model, int deviceType, String languageRegion, String apkPath, String dataDir, String externalDir, String libraryDir, String cacheDir, String shortcutParam, int androidVersion, String board); + public static native void init(String model, int deviceType, String languageRegion, String apkPath, String dataDir, String externalStorageDir, String additionalStorageDirs, String libraryDir, String cacheDir, String shortcutParam, int androidVersion, String board); public static native void audioInit(); public static native void audioShutdown(); public static native void audioConfig(int optimalFramesPerBuffer, int optimalSampleRate); diff --git a/headless/Headless.cpp b/headless/Headless.cpp index 331dfa7cbb9f..bbabad7710bf 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -81,15 +81,11 @@ void NativeRender(GraphicsContext *graphicsContext) { } void NativeResized() { } std::string System_GetProperty(SystemProperty prop) { return ""; } -int System_GetPropertyInt(SystemProperty prop) { - return -1; -} -float System_GetPropertyFloat(SystemProperty prop) { - return -1; -} -bool System_GetPropertyBool(SystemProperty prop) { - return false; -} +std::vector System_GetPropertyStringVec(SystemProperty prop) { return std::vector(); } +int System_GetPropertyInt(SystemProperty prop) { return -1; } +float System_GetPropertyFloat(SystemProperty prop) { return -1.0f; } +bool System_GetPropertyBool(SystemProperty prop) { return false; } + void System_SendMessage(const char *command, const char *parameter) {} void System_InputBoxGetString(const std::string &title, const std::string &defaultValue, std::function cb) { cb(false, ""); } void System_AskForPermission(SystemPermission permission) {} diff --git a/ios/main.mm b/ios/main.mm index 5daecb3e79d9..80825849334c 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -64,6 +64,13 @@ kern_return_t catch_exception_raise(mach_port_t exception_port, } } +std::vector System_GetPropertyStringVec(SystemProperty prop) { + switch (prop) { + default: + return std::vector(); + } +} + int System_GetPropertyInt(SystemProperty prop) { switch (prop) { case SYSPROP_AUDIO_SAMPLE_RATE: diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp index 68f8c0ec7dc3..122cab72e22b 100644 --- a/libretro/libretro.cpp +++ b/libretro/libretro.cpp @@ -905,6 +905,8 @@ float System_GetPropertyFloat(SystemProperty prop) } std::string System_GetProperty(SystemProperty prop) { return ""; } +std::vector System_GetPropertyStringVec(SystemProperty prop) { return std::vector(); } + void System_SendMessage(const char *command, const char *parameter) {} void NativeUpdate() {} void NativeRender(GraphicsContext *graphicsContext) {} diff --git a/unittest/UnitTest.cpp b/unittest/UnitTest.cpp index b5beb3cefa5a..7e5bd98b9772 100644 --- a/unittest/UnitTest.cpp +++ b/unittest/UnitTest.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #if defined(ANDROID) @@ -58,7 +59,9 @@ #include "unittest/TestVertexJit.h" #include "unittest/UnitTest.h" + std::string System_GetProperty(SystemProperty prop) { return ""; } +std::vector System_GetPropertyStringVec(SystemProperty prop) { return std::vector(); } int System_GetPropertyInt(SystemProperty prop) { return -1; }