diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 744e6b9..0ff5714 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ permissions: jobs: build-windows: name: Windows Build - runs-on: windows-latest + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -29,21 +29,74 @@ jobs: with: host: win target: win + + - name: Download xwin + uses: robinraju/release-downloader@v1.10 + with: + repository: Jake-Shadle/xwin + latest: true + fileName: 'xwin-*-x86_64-unknown-linux-musl.tar.gz' + tarBall: false + zipBall: false + out-file-path: "epic-xwin" + + - name: Prepare xwin + run: | + tar -xzf epic-xwin/xwin-*-x86_64-unknown-linux-musl.tar.gz -C epic-xwin + mv epic-xwin/xwin-*-x86_64-unknown-linux-musl/xwin ./xwin + ./xwin --arch x86_64 --sdk-version 10.0.22621 --accept-license list + + # this caches '.xwin-cache/**/*' but that's actually only gonna + # contain the manifest cuz we only ran `xwin list` so far + - name: Setup xwin Cache + id: xwin-cache + uses: actions/cache@v4 + with: + path: .xwin-cache + key: xwin-win-v1-${{ hashFiles('.xwin-cache/**/*') }} + + - name: Download clang-msvc-sdk toolchain + uses: actions/checkout@v4 + with: + repository: 'Nemirtingas/clang-msvc-sdk' + path: toolchain + submodules: recursive + + - name: Install Clang + run: | + sudo apt-get update + sudo apt-get install -y wget lsb-release software-properties-common gnupg + + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh 21 all + + - name: Add LLVM 21 to path + run: echo "/usr/lib/llvm-21/bin" >> $GITHUB_PATH + + - name: Verify LLVM + run: | + which clang-cl + which lld-link + clang-cl --version + lld-link --version - name: Create Build Environment # Some projects don't allow in-source building, so create a separate build directory # We'll use this as our working directory for all subsequent commands run: cmake -E make_directory ${{github.workspace}}/build - + - name: Configure CMake - shell: powershell working-directory: ${{github.workspace}}/build - run: cmake $env:GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$env:BUILD_TYPE + run: cmake $env:GITHUB_WORKSPACE -DCMAKE_BUILD_TYPE=$env:BUILD_TYPE -DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ + env: + SPLAT_DIR: ${{ github.workspace }}/.xwin-cache/splat + TOOLCHAIN: ${{ github.workspace }}/toolchain/clang-cl-msvc.cmake + HOST_ARCH: x86_64 - name: Build working-directory: ${{github.workspace}}/build - shell: powershell - run: cmake --build . --config $env:BUILD_TYPE --target MinecraftPE + run: cmake --build . --config $env:BUILD_TYPE --target MinecraftPE --parallel - name: Upload Artifact uses: actions/upload-artifact@v4 @@ -91,8 +144,8 @@ jobs: working-directory: ${{github.workspace}}/build shell: bash run: | - cmake --build . --config $BUILD_TYPE --target MinecraftPE - cmake --build . --config $BUILD_TYPE --target MinecraftPE-server + cmake --build . --config $BUILD_TYPE --target MinecraftPE --parallel + cmake --build . --config $BUILD_TYPE --target MinecraftPE-server --parallel - name: Upload Artifact uses: actions/upload-artifact@v4 @@ -219,21 +272,13 @@ jobs: - name: Zip Windows Artifacts uses: vimtor/action-zip@v1.2 with: - files: | - ${{github.workspace}}/data/ - mcpe-windows/MinecraftPE.exe - mcpe-windows/glfw3.dll - mcpe-windows/libpng16.dll - mcpe-windows/OpenAL32.dll - mcpe-windows/z.dll + files: data mcpe-windows/MinecraftPE.exe mcpe-windows/glfw3.dll mcpe-windows/libpng16.dll mcpe-windows/OpenAL32.dll mcpe-windows/z.dll dest: minecraftpe-${{ steps.ref.outputs.hash }}-windows.zip - name: Zip Linux Artifacts uses: vimtor/action-zip@v1.2 with: - files: | - ${{github.workspace}}/data/ - mcpe-linux/MinecraftPE # ye, you should install libraries by urself :trollface: + files: data mcpe-linux/MinecraftPE dest: minecraftpe-${{ steps.ref.outputs.hash }}-linux.zip - name: Zip Linux Server Artifacts diff --git a/project/android_java/src/com/mojang/minecraftpe/MainActivity.java b/project/android_java/src/com/mojang/minecraftpe/MainActivity.java index 365fbe5..6dc9e26 100755 --- a/project/android_java/src/com/mojang/minecraftpe/MainActivity.java +++ b/project/android_java/src/com/mojang/minecraftpe/MainActivity.java @@ -59,6 +59,7 @@ public class MainActivity extends Activity { private static final int PERMISSION_REQUEST_CODE = 123; private GLView _glView; + private boolean _nativeInitialized = false; public float invScale = 1.0f;// / 1.5f; private int _screenWidth = 0; private int _screenHeight = 0; @@ -77,63 +78,48 @@ public class MainActivity extends Activity { _screenWidth = Math.max(_dm.widthPixels, _dm.heightPixels); _screenHeight = Math.min(_dm.widthPixels, _dm.heightPixels); - nativeOnCreate(_screenWidth, _screenHeight); - _glView = new GLView(getApplication(), this); //_glView.setEGLConfigChooser(8, 8, 8, 8, 16, 0); + _glView.setEGLConfigChooser(true); //_glView - -// _glView.setEGLConfigChooser( -// new GLSurfaceView.EGLConfigChooser() { -// -// @Override -// public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { -// // TODO Auto-generated method stub -// -// // Specify a configuration for our opengl session -// // and grab the first configuration that matches is -// int[] configSpec = { -// EGL10.EGL_DEPTH_SIZE, 24, -// EGL10.EGL_NONE -// }; -// EGLConfig[] configs = new EGLConfig[1]; -// int[] num_config = new int[1]; -// egl.eglChooseConfig(display, configSpec, configs, 1, num_config); -// EGLConfig config = configs[0]; -// return config; -// -// //return null; -// } -// } ); _glView.commit(); - setContentView(_glView); - - _soundPlayer = new SoundPlayer(this, AudioManager.STREAM_MUSIC); + setContentView(_glView); + + _soundPlayer = new SoundPlayer(this, AudioManager.STREAM_MUSIC); - // request dangerous permissions at runtime if necessary checkAndRequestPermissions(); + initNative(); } - private void checkAndRequestPermissions() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - boolean needRequest = false; - if (checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - needRequest = true; - } - if (checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - needRequest = true; - } - if (needRequest) { - requestPermissions(new String[] { - android.Manifest.permission.WRITE_EXTERNAL_STORAGE, - android.Manifest.permission.READ_EXTERNAL_STORAGE - }, PERMISSION_REQUEST_CODE); - } + private void initNative() { + if (_nativeInitialized) { + return; } + _nativeInitialized = true; + nativeOnCreate(_screenWidth, _screenHeight); + } + + // request dangerous permissions at runtime if necessary + private boolean checkAndRequestPermissions() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + return true; + } + + boolean writeGranted = checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + boolean readGranted = checkSelfPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + + if (writeGranted && readGranted) { + return true; + } + + requestPermissions(new String[] { + android.Manifest.permission.WRITE_EXTERNAL_STORAGE, + android.Manifest.permission.READ_EXTERNAL_STORAGE + }, PERMISSION_REQUEST_CODE); + + return false; } @Override @@ -146,8 +132,18 @@ public class MainActivity extends Activity { break; } } - if (!granted) { - // user denied; you might want to warn or close the app + if (granted) { + initNative(); + } else { + // We can still run using app-specific external files in scoped-storage, + // so allow startup while warning the user. + initNative(); + new AlertDialog.Builder(this) + .setTitle("Storage permission recommended") + .setMessage("MinecraftPE can still run with app-private storage, but public external save/load may require permission.") + .setPositiveButton("OK", null) + .setCancelable(true) + .show(); } } super.onRequestPermissionsResult(requestCode, permissions, grantResults); diff --git a/src/AppPlatform_glfw.h b/src/AppPlatform_glfw.h index dcba807..4b298c0 100755 --- a/src/AppPlatform_glfw.h +++ b/src/AppPlatform_glfw.h @@ -73,50 +73,20 @@ public: : filename_; std::ifstream source(filename.c_str(), std::ios::binary); - if (source) { - png_structp pngPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); - - if (!pngPtr) - return out; - - png_infop infoPtr = png_create_info_struct(pngPtr); - - if (!infoPtr) { - png_destroy_read_struct(&pngPtr, NULL, NULL); - return out; - } - - // Hack to get around the broken libpng for windows - png_set_read_fn(pngPtr,(void*)&source, png_funcReadFile); - - png_read_info(pngPtr, infoPtr); - - // Set up the texdata properties - out.w = png_get_image_width(pngPtr, infoPtr); - out.h = png_get_image_height(pngPtr, infoPtr); - - png_bytep* rowPtrs = new png_bytep[out.h]; - out.data = new unsigned char[4 * out.w * out.h]; - out.memoryHandledExternally = false; - - int rowStrideBytes = 4 * out.w; - for (int i = 0; i < out.h; i++) { - rowPtrs[i] = (png_bytep)&out.data[i*rowStrideBytes]; - } - png_read_image(pngPtr, rowPtrs); - - // Teardown and return - png_destroy_read_struct(&pngPtr, &infoPtr,(png_infopp)0); - delete[] (png_bytep)rowPtrs; - source.close(); - - return out; - } - else - { + if (!source) { LOGI("Couldn't find file: %s\n", filename.c_str()); return out; } + + std::vector fileData((std::istreambuf_iterator(source)), std::istreambuf_iterator()); + source.close(); + + if (fileData.empty()) { + LOGI("Couldn't read file: %s\n", filename.c_str()); + return out; + } + + return loadTextureFromMemory(fileData.data(), fileData.size()); } TextureData loadTextureFromMemory(const unsigned char* data, size_t size) override { diff --git a/src/NinecraftApp.cpp b/src/NinecraftApp.cpp index 69e01c3..d2feea6 100755 --- a/src/NinecraftApp.cpp +++ b/src/NinecraftApp.cpp @@ -99,6 +99,9 @@ void NinecraftApp::init() I18n::loadLanguage(platform(), "en_US"); #endif + if (!externalStoragePath.empty()) { + options.setOptionsFilePath(externalStoragePath); + } Minecraft::init(); #if !defined(DEMO_MODE) && !defined(APPLE_DEMO_PROMOTION) && !defined(NO_STORAGE) diff --git a/src/client/Minecraft.cpp b/src/client/Minecraft.cpp index ba8b126..926ff10 100755 --- a/src/client/Minecraft.cpp +++ b/src/client/Minecraft.cpp @@ -1143,6 +1143,8 @@ void Minecraft::init() checkGlError("Init complete"); #endif + options.load(); + setIsCreativeMode(false); // false means it's Survival Mode reloadOptions(); } diff --git a/src/client/Options.cpp b/src/client/Options.cpp index 478e2ca..2cacca0 100755 --- a/src/client/Options.cpp +++ b/src/client/Options.cpp @@ -285,6 +285,10 @@ void Options::save() { optionsFile.save(stringVec); } +void Options::setOptionsFilePath(const std::string& path) { + optionsFile.setOptionsPath(path + "/options.txt"); +} + void Options::notifyOptionUpdate(OptionId key, bool value) { minecraft->optionUpdated(key, value); } diff --git a/src/client/Options.h b/src/client/Options.h index 670cca7..f5a4393 100755 --- a/src/client/Options.h +++ b/src/client/Options.h @@ -101,15 +101,10 @@ public: // elements werent initialized so i was getting a garbage pointer and a crash m_options.fill(nullptr); initTable(); - load(); + // load() is deferred to init() where path is configured correctly } - void initTable(); - - void set(OptionId key, int value); - void set(OptionId key, float value); - void set(OptionId key, const std::string& value); - void toggle(OptionId key); + void initTable(); int getIntValue(OptionId key) { auto option = opt(key); @@ -145,6 +140,11 @@ public: void load(); void save(); + void set(OptionId key, int value); + void set(OptionId key, float value); + void set(OptionId key, const std::string& value); + void setOptionsFilePath(const std::string& path); + void toggle(OptionId key); void notifyOptionUpdate(OptionId key, bool value); void notifyOptionUpdate(OptionId key, float value); diff --git a/src/client/OptionsFile.cpp b/src/client/OptionsFile.cpp index 3bd9a4a..feb2fdf 100755 --- a/src/client/OptionsFile.cpp +++ b/src/client/OptionsFile.cpp @@ -4,6 +4,12 @@ #include #include +#if !defined(_WIN32) +#include +#include +#include +#endif + OptionsFile::OptionsFile() { #ifdef __APPLE__ settingsPath = "./Documents/options.txt"; @@ -14,6 +20,14 @@ OptionsFile::OptionsFile() { #endif } +void OptionsFile::setOptionsPath(const std::string& path) { + settingsPath = path; +} + +std::string OptionsFile::getOptionsPath() const { + return settingsPath; +} + void OptionsFile::save(const StringVector& settings) { FILE* pFile = fopen(settingsPath.c_str(), "w"); if(pFile != NULL) { @@ -22,10 +36,33 @@ void OptionsFile::save(const StringVector& settings) { } fclose(pFile); } else { - LOGI("OptionsFile::save failed to open '%s' for writing: %s", settingsPath.c_str(), strerror(errno)); + if (errno != ENOENT) + LOGI("OptionsFile::save failed to open '%s' for writing: %s", settingsPath.c_str(), strerror(errno)); + + // Ensure parent directory exists for safekeeping if path contains directories + std::string dir = settingsPath; + size_t fpos = dir.find_last_of("/\\"); + if (fpos != std::string::npos) { + dir.resize(fpos); + struct stat st; + if (stat(dir.c_str(), &st) != 0) { + // attempt recursive mkdir + std::string toCreate; + for (size_t i = 0; i <= dir.size(); ++i) { + if (i == dir.size() || dir[i] == '/' || dir[i] == '\\') { + if (!toCreate.empty()) { + mkdir(toCreate.c_str(), 0755); + } + } + if (i < dir.size()) + toCreate.push_back(dir[i]); + } + } + } } } + StringVector OptionsFile::getOptionStrings() { StringVector returnVector; FILE* pFile = fopen(settingsPath.c_str(), "r"); @@ -46,7 +83,8 @@ StringVector OptionsFile::getOptionStrings() { } fclose(pFile); } else { - LOGI("OptionsFile::getOptionStrings failed to open '%s' for reading: %s", settingsPath.c_str(), strerror(errno)); + if (errno != ENOENT) + LOGI("OptionsFile::getOptionStrings failed to open '%s' for reading: %s", settingsPath.c_str(), strerror(errno)); } return returnVector; } diff --git a/src/client/OptionsFile.h b/src/client/OptionsFile.h index 942af75..b2464a0 100755 --- a/src/client/OptionsFile.h +++ b/src/client/OptionsFile.h @@ -11,7 +11,9 @@ public: OptionsFile(); void save(const StringVector& settings); StringVector getOptionStrings(); - + void setOptionsPath(const std::string& path); + std::string getOptionsPath() const; + private: std::string settingsPath; }; diff --git a/src/client/player/LocalPlayer.cpp b/src/client/player/LocalPlayer.cpp index c048801..b4d57ef 100755 --- a/src/client/player/LocalPlayer.cpp +++ b/src/client/player/LocalPlayer.cpp @@ -187,6 +187,33 @@ static bool ensureDirectoryExists(const std::string& path) { return _mkdir(path.c_str()) == 0 || errno == EEXIST; #else struct stat st; + if (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) + return true; + + std::string subPath; + size_t i = 0; + while (i < path.length()) { + i = path.find_first_of("/\\", i); + if (i == std::string::npos) { + subPath = path; + } else { + subPath = path.substr(0, i); + } + + if (!subPath.empty()) { + if (stat(subPath.c_str(), &st) != 0) { + if (mkdir(subPath.c_str(), 0755) != 0 && errno != EEXIST) + return false; + } else if (!S_ISDIR(st.st_mode)) { + return false; + } + } + + if (i == std::string::npos) + break; + i += 1; + } + if (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) return true; return mkdir(path.c_str(), 0755) == 0 || errno == EEXIST; @@ -236,7 +263,8 @@ static void* fetchSkinForPlayer(void* param) { std::vector skinData; if (!HttpClient::download(skinUrl, skinData) || skinData.empty()) { LOGW("[Skin] download failed for %s\n", skinUrl.c_str()); - return NULL; + player->setTextureName("mob/char.png"); + return NULL; } // Save to cache diff --git a/src/client/renderer/entity/HumanoidMobRenderer.h b/src/client/renderer/entity/HumanoidMobRenderer.h index e10c27c..a78ade8 100755 --- a/src/client/renderer/entity/HumanoidMobRenderer.h +++ b/src/client/renderer/entity/HumanoidMobRenderer.h @@ -19,12 +19,13 @@ public: protected: void additionalRendering(Mob* mob, float a); -private: HumanoidModel* humanoidModel; // Last rotation values for cape smoothing (reduces jitter) float lastCapeXRot; float lastCapeZRot; +private: + // i guess ill keep this just in case seomthing breaks }; #endif /*NET_MINECRAFT_CLIENT_RENDERER_ENTITY__HumanoidMobRenderer_H__*/ diff --git a/src/client/renderer/entity/MobRenderer.h b/src/client/renderer/entity/MobRenderer.h index 3f22618..fee7786 100755 --- a/src/client/renderer/entity/MobRenderer.h +++ b/src/client/renderer/entity/MobRenderer.h @@ -43,8 +43,8 @@ public: protected: void setArmor(Model* armor); Model* getArmor(); + Model* model; // allows derived renderers to swap models dynamically for skin formats private: - Model* model; Model* armor; }; diff --git a/src/client/renderer/entity/PlayerRenderer.cpp b/src/client/renderer/entity/PlayerRenderer.cpp index b184bcb..6a773f0 100755 --- a/src/client/renderer/entity/PlayerRenderer.cpp +++ b/src/client/renderer/entity/PlayerRenderer.cpp @@ -1,5 +1,6 @@ #include "PlayerRenderer.h" #include "EntityRenderDispatcher.h" +#include "../Textures.h" #include "../../../world/entity/player/Player.h" #include "../../../world/level/Level.h" #include "../../../world/item/ArmorItem.h" @@ -14,12 +15,22 @@ static const std::string armorFilenames[10] = { PlayerRenderer::PlayerRenderer( HumanoidModel* humanoidModel, float shadow ) : super(humanoidModel, shadow), + playerModel64(humanoidModel), + playerModel32(new HumanoidModel(0, 0, 64, 32)), armorParts1(new HumanoidModel(1.0f, 0, 64, 64)), armorParts2(new HumanoidModel(0.5f, 0, 64, 64)) { + // default to legacy skin path until we know the exact texture size + model = playerModel32; + humanoidModel = playerModel32; } PlayerRenderer::~PlayerRenderer() { + // prevent MobRenderer destructor from deleting model pointers we manage manually + model = nullptr; + + delete playerModel32; + delete playerModel64; delete armorParts1; delete armorParts2; } @@ -43,6 +54,15 @@ void PlayerRenderer::setupRotations( Entity* mob, float bob, float bodyRot, floa super::setupRotations(mob, bob, bodyRot, a); } +bool PlayerRenderer::isModernPlayerSkin(Mob* mob) { + const std::string texName = mob->getTexture(); + TextureId texId = entityRenderDispatcher->textures->loadTexture(texName); + if (!Textures::isTextureIdValid(texId)) + return false; + const TextureData* texData = entityRenderDispatcher->textures->getTemporaryTextureData(texId); + return texData && texData->w == 64 && texData->h == 64; +} + void PlayerRenderer::renderName( Mob* mob, float x, float y, float z ){ //@todo: figure out how to handle HideGUI if (mob != entityRenderDispatcher->cameraEntity && mob->level->adventureSettings.showNameTags) { @@ -50,6 +70,20 @@ void PlayerRenderer::renderName( Mob* mob, float x, float y, float z ){ } } +void PlayerRenderer::render(Entity* mob_, float x, float y, float z, float rot, float a) { + Mob* mob = (Mob*) mob_; + HumanoidModel* desired = isModernPlayerSkin(mob) ? playerModel64 : playerModel32; + if (model != desired || humanoidModel != desired) { + model = desired; + humanoidModel = desired; + } + // LOGI("[PlayerRenderer] %s: skin=%s, modelTex=%dx%d, desired=%s\n", + // ((Player*)mob)->name.c_str(), mob->getTexture().c_str(), + // humanoidModel->texWidth, humanoidModel->texHeight, + // (desired == playerModel64 ? "64" : "32")); + HumanoidMobRenderer::render(mob_, x, y, z, rot, a); +} + int PlayerRenderer::prepareArmor(Mob* mob, int layer, float a) { Player* player = (Player*) mob; @@ -80,8 +114,11 @@ int PlayerRenderer::prepareArmor(Mob* mob, int layer, float a) { } void PlayerRenderer::onGraphicsReset() { - super::onGraphicsReset(); + if (playerModel32) playerModel32->onGraphicsReset(); + if (playerModel64) playerModel64->onGraphicsReset(); if (armorParts1) armorParts1->onGraphicsReset(); if (armorParts2) armorParts2->onGraphicsReset(); + + super::onGraphicsReset(); } diff --git a/src/client/renderer/entity/PlayerRenderer.h b/src/client/renderer/entity/PlayerRenderer.h index f370607..d4b25e2 100755 --- a/src/client/renderer/entity/PlayerRenderer.h +++ b/src/client/renderer/entity/PlayerRenderer.h @@ -11,6 +11,8 @@ public: ~PlayerRenderer(); virtual int prepareArmor(Mob* mob, int layer, float a); + bool isModernPlayerSkin(Mob* mob); + virtual void render(Entity* mob, float x, float y, float z, float rot, float a); virtual void setupPosition(Entity* mob, float x, float y, float z); virtual void setupRotations(Entity* mob, float bob, float bodyRot, float a); @@ -18,6 +20,8 @@ public: virtual void renderName(Mob* mob, float x, float y, float z); virtual void onGraphicsReset(); private: + HumanoidModel* playerModel32; + HumanoidModel* playerModel64; HumanoidModel* armorParts1; HumanoidModel* armorParts2; }; diff --git a/src/main_android.cpp b/src/main_android.cpp index 7d9c2bb..dc47f35 100755 --- a/src/main_android.cpp +++ b/src/main_android.cpp @@ -25,12 +25,33 @@ static void setupExternalPath(struct android_app* state, MAIN_CLASS* app) { LOGI("Environment exists"); } - jclass clazz = env->FindClass("android/os/Environment"); - jmethodID method = env->GetStaticMethodID(clazz, "getExternalStorageDirectory", "()Ljava/io/File;"); - if (env->ExceptionOccurred()) { - env->ExceptionDescribe(); + // try appspecific external directory first + jobject activity = state->activity->clazz; + jclass activityClass = env->GetObjectClass(activity); + jmethodID getExternalFilesDir = env->GetMethodID(activityClass, "getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;"); + + jobject file = NULL; + if (getExternalFilesDir != NULL) { + file = env->CallObjectMethod(activity, getExternalFilesDir, NULL); + } + + if (file == NULL) { + // Fallback to the legacy shared storage directory + jclass clazz = env->FindClass("android/os/Environment"); + jmethodID method = env->GetStaticMethodID(clazz, "getExternalStorageDirectory", "()Ljava/io/File;"); + if (env->ExceptionOccurred()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + file = env->CallStaticObjectMethod(clazz, method); + } + + if (!file) { + LOGI("Failed to get external storage file object, using current working dir"); + app->externalStoragePath = "."; + app->externalCacheStoragePath = "."; + return; } - jobject file = env->CallStaticObjectMethod(clazz, method); jclass fileClass = env->GetObjectClass(file); jmethodID fileMethod = env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;"); @@ -38,7 +59,7 @@ static void setupExternalPath(struct android_app* state, MAIN_CLASS* app) const char* str = env->GetStringUTFChars((jstring) pathString, NULL); app->externalStoragePath = str; - app->externalCacheStoragePath = str; + app->externalCacheStoragePath = str; LOGI("%s", str); // ensure the process working directory is set to a writable location diff --git a/src/main_android_java.cpp b/src/main_android_java.cpp index e6d91df..c89ae03 100755 --- a/src/main_android_java.cpp +++ b/src/main_android_java.cpp @@ -30,12 +30,33 @@ static void setupExternalPath(JNIEnv* env, MAIN_CLASS* app) { LOGI("Environment exists"); } - jclass clazz = env->FindClass("android/os/Environment"); - jmethodID method = env->GetStaticMethodID(clazz, "getExternalStorageDirectory", "()Ljava/io/File;"); - if (env->ExceptionOccurred()) { - env->ExceptionDescribe(); + // try appspecific external directory first + jobject activity = g_pActivity; + jclass activityClass = env->GetObjectClass(activity); + jmethodID getExternalFilesDir = env->GetMethodID(activityClass, "getExternalFilesDir", "(Ljava/lang/String;)Ljava/io/File;"); + + jobject file = NULL; + if (getExternalFilesDir != NULL) { + file = env->CallObjectMethod(activity, getExternalFilesDir, NULL); + } + + if (file == NULL) { + // Fallback to the legacy shared storage directory + jclass clazz = env->FindClass("android/os/Environment"); + jmethodID method = env->GetStaticMethodID(clazz, "getExternalStorageDirectory", "()Ljava/io/File;"); + if (env->ExceptionOccurred()) { + env->ExceptionDescribe(); + env->ExceptionClear(); + } + file = env->CallStaticObjectMethod(clazz, method); + } + + if (!file) { + LOGI("Failed to get external storage file object, using current working dir"); + app->externalStoragePath = "."; + app->externalCacheStoragePath = "."; + return; } - jobject file = env->CallStaticObjectMethod(clazz, method); jclass fileClass = env->GetObjectClass(file); jmethodID fileMethod = env->GetMethodID(fileClass, "getAbsolutePath", "()Ljava/lang/String;"); @@ -43,7 +64,7 @@ static void setupExternalPath(JNIEnv* env, MAIN_CLASS* app) const char* str = env->GetStringUTFChars((jstring) pathString, NULL); app->externalStoragePath = str; - app->externalCacheStoragePath = str; + app->externalCacheStoragePath = str; LOGI("%s", str); // same fix as the native entry point: make sure cwd is writable