diff --git a/NEWS b/NEWS index c6e18a8be..08266f358 100644 --- a/NEWS +++ b/NEWS @@ -18,7 +18,7 @@ ver 0.24 (not yet released) * static partition configuration * remove Haiku support -ver 0.23.9 (not yet released) +ver 0.23.9 (2022/08/18) * input - cdio_paranoia: add options "mode" and "skip" * decoder @@ -30,6 +30,8 @@ ver 0.23.9 (not yet released) * fix bogus volume levels with multiple partitions * improve iconv detection * macOS: fix macOS 10 build problem (0.23.8 regression) +* Android + - load mpd.conf from app data directory ver 0.23.8 (2022/07/09) * storage diff --git a/doc/user.rst b/doc/user.rst index 5280bf659..5bb4ca793 100644 --- a/doc/user.rst +++ b/doc/user.rst @@ -36,7 +36,9 @@ Installing on Android An experimental Android build is available on Google Play. After installing and launching it, :program:`MPD` will scan the music in your Music directory and you can control it as usual with a :program:`MPD` client. -If you need to tweak the configuration, you can create a file called :file:`mpd.conf` on the data partition (the directory which is returned by Android's :dfn:`getExternalStorageDirectory()` API function). +If you need to tweak the configuration, you can create a file called +:file:`mpd.conf` in MPD's data directory on the external storage +(usually :file:`Android/data/org.musicpd/files/mpd.conf`). ALSA is not available on Android; only the :ref:`OpenSL ES ` output plugin can be used for local playback. diff --git a/src/Main.cxx b/src/Main.cxx index 170313f4d..e5c2d8f08 100644 --- a/src/Main.cxx +++ b/src/Main.cxx @@ -540,19 +540,46 @@ MainConfigured(const CommandLineOptions &options, #ifdef ANDROID +/** + * Wrapper for ReadConfigFile() which returns false if the file was + * not found. + */ +static bool +TryReadConfigFile(ConfigData &config, Path path) +{ + if (!FileExists(path)) + return false; + + ReadConfigFile(config, path); + return true; +} + static void -AndroidMain() +LoadConfigFile(JNIEnv *env, ConfigData &config) +{ + /* try loading mpd.conf from + "Android/data/org.musicpd/files/mpd.conf" (the app specific + data directory) first */ + if (const auto dir = context->GetExternalFilesDir(env); + !dir.IsNull() && + TryReadConfigFile(config, dir / Path::FromFS("mpd.conf"))) + return; + + /* if that fails, attempt to load "mpd.conf" from the root of + the SD card (pre-0.23.9, ceases to work since Android + 12) */ + if (const auto dir = Environment::getExternalStorageDirectory(env); + !dir.IsNull()) + TryReadConfigFile(config, dir / Path::FromFS("mpd.conf")); +} + +static void +AndroidMain(JNIEnv *env) { CommandLineOptions options; ConfigData raw_config; - const auto sdcard = Environment::getExternalStorageDirectory(); - if (!sdcard.IsNull()) { - const auto config_path = - sdcard / Path::FromFS("mpd.conf"); - if (FileExists(config_path)) - ReadConfigFile(raw_config, config_path); - } + LoadConfigFile(env, raw_config); MainConfigured(options, raw_config); } @@ -564,9 +591,12 @@ Java_org_musicpd_Bridge_run(JNIEnv *env, jclass, jobject _context, jobject _logL Java::Init(env); Java::Object::Initialise(env); Java::File::Initialise(env); + Environment::Initialise(env); AtScopeExit(env) { Environment::Deinitialise(env); }; + Context::Initialise(env); + context = new Context(env, _context); AtScopeExit() { delete context; }; @@ -575,7 +605,7 @@ Java_org_musicpd_Bridge_run(JNIEnv *env, jclass, jobject _context, jobject _logL AtScopeExit() { delete logListener; }; try { - AndroidMain(); + AndroidMain(env); } catch (...) { LogError(std::current_exception()); } diff --git a/src/android/Context.cxx b/src/android/Context.cxx index fd1376a8c..0a31bf58c 100644 --- a/src/android/Context.cxx +++ b/src/android/Context.cxx @@ -26,19 +26,30 @@ #include "AudioManager.hxx" +static jmethodID getExternalFilesDir_method, + getCacheDir_method, + getSystemService_method; + +void +Context::Initialise(JNIEnv *env) noexcept +{ + Java::Class cls{env, "android/content/Context"}; + + getExternalFilesDir_method = env->GetMethodID(cls, "getExternalFilesDir", + "(Ljava/lang/String;)Ljava/io/File;"); + getCacheDir_method = env->GetMethodID(cls, "getCacheDir", + "()Ljava/io/File;"); + getSystemService_method = env->GetMethodID(cls, "getSystemService", + "(Ljava/lang/String;)Ljava/lang/Object;"); +} + AllocatedPath -Context::GetExternalFilesDir(JNIEnv *env, const char *_type) noexcept +Context::GetExternalFilesDir(JNIEnv *env, const char *type) noexcept { assert(_type != nullptr); - Java::Class cls{env, env->GetObjectClass(Get())}; - jmethodID method = env->GetMethodID(cls, "getExternalFilesDir", - "(Ljava/lang/String;)Ljava/io/File;"); - assert(method); - - Java::String type{env, _type}; - - jobject file = env->CallObjectMethod(Get(), method, type.Get()); + jobject file = env->CallObjectMethod(Get(), getExternalFilesDir_method, + Java::String::Optional(env, type).Get()); if (Java::DiscardException(env) || file == nullptr) return nullptr; @@ -50,12 +61,7 @@ Context::GetCacheDir(JNIEnv *env) const noexcept { assert(env != nullptr); - Java::Class cls(env, env->GetObjectClass(Get())); - jmethodID method = env->GetMethodID(cls, "getCacheDir", - "()Ljava/io/File;"); - assert(method); - - jobject file = env->CallObjectMethod(Get(), method); + jobject file = env->CallObjectMethod(Get(), getCacheDir_method); if (Java::DiscardException(env) || file == nullptr) return nullptr; @@ -67,13 +73,8 @@ Context::GetAudioManager(JNIEnv *env) noexcept { assert(env != nullptr); - Java::Class cls(env, env->GetObjectClass(Get())); - jmethodID method = env->GetMethodID(cls, "getSystemService", - "(Ljava/lang/String;)Ljava/lang/Object;"); - assert(method); - Java::String name(env, "audio"); - jobject am = env->CallObjectMethod(Get(), method, name.Get()); + jobject am = env->CallObjectMethod(Get(), getSystemService_method, name.Get()); if (Java::DiscardException(env) || am == nullptr) return nullptr; diff --git a/src/android/Context.hxx b/src/android/Context.hxx index 8ac01a3ab..cc137458e 100644 --- a/src/android/Context.hxx +++ b/src/android/Context.hxx @@ -27,12 +27,21 @@ class AudioManager; class Context : public Java::GlobalObject { public: + /** + * Global initialisation. Looks up the methods of the + * Context Java class. + */ + static void Initialise(JNIEnv *env) noexcept; + Context(JNIEnv *env, jobject obj) noexcept :Java::GlobalObject(env, obj) {} + /** + * @param type the subdirectory name; may be nullptr + */ [[gnu::pure]] AllocatedPath GetExternalFilesDir(JNIEnv *env, - const char *type) noexcept; + const char *type=nullptr) noexcept; [[gnu::pure]] AllocatedPath GetCacheDir(JNIEnv *env) const noexcept; diff --git a/src/android/Environment.cxx b/src/android/Environment.cxx index bb6d8e881..2554056a1 100644 --- a/src/android/Environment.cxx +++ b/src/android/Environment.cxx @@ -25,13 +25,13 @@ #include "fs/AllocatedPath.hxx" namespace Environment { - static Java::TrivialClass cls; - static jmethodID getExternalStorageDirectory_method; - static jmethodID getExternalStoragePublicDirectory_method; -} + +static Java::TrivialClass cls; +static jmethodID getExternalStorageDirectory_method; +static jmethodID getExternalStoragePublicDirectory_method; void -Environment::Initialise(JNIEnv *env) noexcept +Initialise(JNIEnv *env) noexcept { cls.Find(env, "android/os/Environment"); @@ -45,16 +45,14 @@ Environment::Initialise(JNIEnv *env) noexcept } void -Environment::Deinitialise(JNIEnv *env) noexcept +Deinitialise(JNIEnv *env) noexcept { cls.Clear(env); } AllocatedPath -Environment::getExternalStorageDirectory() noexcept +getExternalStorageDirectory(JNIEnv *env) noexcept { - JNIEnv *env = Java::GetEnv(); - jobject file = env->CallStaticObjectMethod(cls, getExternalStorageDirectory_method); @@ -65,20 +63,20 @@ Environment::getExternalStorageDirectory() noexcept } AllocatedPath -Environment::getExternalStoragePublicDirectory(const char *type) noexcept +getExternalStoragePublicDirectory(JNIEnv *env, const char *type) noexcept { if (getExternalStoragePublicDirectory_method == nullptr) /* needs API level 8 */ return nullptr; - JNIEnv *env = Java::GetEnv(); - Java::String type2(env, type); - jobject file = env->CallStaticObjectMethod(Environment::cls, - Environment::getExternalStoragePublicDirectory_method, + jobject file = env->CallStaticObjectMethod(cls, + getExternalStoragePublicDirectory_method, type2.Get()); if (file == nullptr) return nullptr; return Java::File::ToAbsolutePath(env, file); } + +} // namespace Environment diff --git a/src/android/Environment.hxx b/src/android/Environment.hxx index 61ad57bb1..58a3f77a7 100644 --- a/src/android/Environment.hxx +++ b/src/android/Environment.hxx @@ -17,27 +17,29 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ -#ifndef MPD_ANDROID_ENVIRONMENT_HXX -#define MPD_ANDROID_ENVIRONMENT_HXX - -#include "util/Compiler.h" +#pragma once #include class AllocatedPath; namespace Environment { - void Initialise(JNIEnv *env) noexcept; - void Deinitialise(JNIEnv *env) noexcept; - /** - * Determine the mount point of the external SD card. - */ - [[gnu::pure]] - AllocatedPath getExternalStorageDirectory() noexcept; +void +Initialise(JNIEnv *env) noexcept; - [[gnu::pure]] - AllocatedPath getExternalStoragePublicDirectory(const char *type) noexcept; -} +void +Deinitialise(JNIEnv *env) noexcept; -#endif +/** + * Determine the mount point of the external SD card. + */ +[[gnu::pure]] +AllocatedPath +getExternalStorageDirectory(JNIEnv *env) noexcept; + +[[gnu::pure]] +AllocatedPath +getExternalStoragePublicDirectory(JNIEnv *env, const char *type) noexcept; + +} // namespace Environment diff --git a/src/fs/StandardDirectory.cxx b/src/fs/StandardDirectory.cxx index 986a8ab58..4782b4da9 100644 --- a/src/fs/StandardDirectory.cxx +++ b/src/fs/StandardDirectory.cxx @@ -254,7 +254,8 @@ GetUserMusicDir() noexcept #elif defined(USE_XDG) return GetUserDir("XDG_MUSIC_DIR"); #elif defined(ANDROID) - return Environment::getExternalStoragePublicDirectory("Music"); + return Environment::getExternalStoragePublicDirectory(Java::GetEnv(), + "Music"); #else return nullptr; #endif diff --git a/src/java/String.hxx b/src/java/String.hxx index 34df72feb..1a6e5501c 100644 --- a/src/java/String.hxx +++ b/src/java/String.hxx @@ -92,6 +92,16 @@ public: String(JNIEnv *_env, std::string_view _value) noexcept; + /** + * This constructor allows passing a nullptr value, which maps + * to a "null" in Java. + */ + static String Optional(JNIEnv *_env, const char *_value) noexcept { + return _value != nullptr + ? String{_env, _value} + : String{}; + } + static StringUTFChars GetUTFChars(JNIEnv *env, jstring s) noexcept { return {env, s, env->GetStringUTFChars(s, nullptr)}; }