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.
This commit is contained in:
Henrik Rydgård 2021-01-06 16:37:04 +01:00
parent 5929c78307
commit 1c753e4a22
13 changed files with 202 additions and 26 deletions

View File

@ -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<std::string> 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<std::string> System_GetPropertyStringVec(SystemProperty prop);
int System_GetPropertyInt(SystemProperty prop);
float System_GetPropertyFloat(SystemProperty prop);
bool System_GetPropertyBool(SystemProperty prop);

View File

@ -149,6 +149,13 @@ std::string System_GetProperty(SystemProperty prop) {
}
}
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) {
switch (prop) {
default:
return std::vector<std::string>();
}
}
int System_GetPropertyInt(SystemProperty prop) {
switch (prop) {
#if defined(SDL)

View File

@ -325,6 +325,13 @@ std::string System_GetProperty(SystemProperty prop) {
}
}
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) {
switch (prop) {
default:
return std::vector<std::string>();
}
}
int System_GetPropertyInt(SystemProperty prop) {
switch (prop) {
case SYSPROP_AUDIO_SAMPLE_RATE:

View File

@ -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<std::string> 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);

View File

@ -356,6 +356,13 @@ std::string System_GetProperty(SystemProperty prop) {
}
}
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) {
switch (prop) {
default:
return std::vector<std::string>();
}
}
int System_GetPropertyInt(SystemProperty prop) {
switch (prop) {
case SYSPROP_AUDIO_SAMPLE_RATE:

View File

@ -216,6 +216,13 @@ std::string System_GetProperty(SystemProperty prop) {
}
}
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) {
switch (prop) {
default:
return std::vector<std::string>();
}
}
// Ugly!
extern WindowsAudioBackend *winAudioBackend;

View File

@ -13,6 +13,8 @@
#include <thread>
#include <atomic>
#include <android/log.h>
#ifndef _MSC_VER
#include <jni.h>
#include <android/native_window_jni.h>
@ -122,6 +124,8 @@ std::string langRegion;
std::string mogaVersion;
std::string boardName;
std::vector<std::string> 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<std::string> System_GetPropertyStringVec(SystemProperty prop) {
switch (prop) {
case SYSPROP_ADDITIONAL_STORAGE_DIRS:
return g_additionalStorageDirs;
default:
return std::vector<std::string>();
}
}
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<std::string> &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.

View File

@ -46,6 +46,7 @@ import android.widget.Toast;
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 abstract class NativeActivity extends Activity {
this.shortcutParam = ((shortcutParam == null) ? "" : shortcutParam);
}
// Unofficial hacks to get a list of SD cards that are not the main "external storage".
private static List<String> getSdCardPaths(final Context context) {
// Q is the last version that will support normal file access.
List<String> 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<String>();
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<String>();
} 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<String> 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<String> 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 abstract class NativeActivity extends Activity {
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<String> 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 abstract class NativeActivity extends Activity {
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"));

View File

@ -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);

View File

@ -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<std::string> System_GetPropertyStringVec(SystemProperty prop) { return std::vector<std::string>(); }
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<void(bool, const std::string &)> cb) { cb(false, ""); }
void System_AskForPermission(SystemPermission permission) {}

View File

@ -64,6 +64,13 @@ std::string System_GetProperty(SystemProperty prop) {
}
}
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) {
switch (prop) {
default:
return std::vector<std::string>();
}
}
int System_GetPropertyInt(SystemProperty prop) {
switch (prop) {
case SYSPROP_AUDIO_SAMPLE_RATE:

View File

@ -905,6 +905,8 @@ float System_GetPropertyFloat(SystemProperty prop)
}
std::string System_GetProperty(SystemProperty prop) { return ""; }
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) { return std::vector<std::string>(); }
void System_SendMessage(const char *command, const char *parameter) {}
void NativeUpdate() {}
void NativeRender(GraphicsContext *graphicsContext) {}

View File

@ -30,6 +30,7 @@
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <vector>
#include <string>
#include <sstream>
#if defined(ANDROID)
@ -58,7 +59,9 @@
#include "unittest/TestVertexJit.h"
#include "unittest/UnitTest.h"
std::string System_GetProperty(SystemProperty prop) { return ""; }
std::vector<std::string> System_GetPropertyStringVec(SystemProperty prop) { return std::vector<std::string>(); }
int System_GetPropertyInt(SystemProperty prop) {
return -1;
}