/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include //#define LOG_NDEBUG 0 #define LOG_TAG "SoundPool-JNI" #include #include #include #include #include #include #include "SoundPool.h" using namespace android; static struct fields_t { jfieldID mNativeContext; jmethodID mPostEvent; jclass mSoundPoolClass; } fields; namespace { /** * ObjectManager creates a native "object" on the heap and stores * its pointer in a long field in a Java object. * * The type T must have 3 properties in the current implementation. * 1) A T{} default constructor which represents a nullValue. * 2) T::operator bool() const efficient detection of such a nullValue. * 3) T must be copyable. * * Some examples of such a type T are std::shared_ptr<>, android::sp<>, * std::optional, std::function<>, etc. * * Using set() with a nullValue T results in destroying the underlying native * "object" if it exists. A nullValue T is returned by get() if there is * no underlying native Object. * * This class is thread safe for multiple access. * * Design notes: * 1) For objects of type T that do not naturally have an "nullValue", * wrapping with * a) TOpt, where TOpt = std::optional * b) TShared, where TShared = std::shared_ptr * * 2) An overload for an explicit equality comparable nullValue such as * get(..., const T& nullValue) or set(..., const T& nullValue) * is omitted. An alternative is to pass a fixed nullValue in the constructor. */ template class ObjectManager { // Can a jlong hold a pointer? static_assert(sizeof(jlong) >= sizeof(void*)); public: // fieldId is associated with a Java long member variable in the object. // ObjectManager will store the native pointer in that field. // // If a native object is set() in that field, it explicit ObjectManager(jfieldID fieldId) : mFieldId(fieldId) {} ~ObjectManager() { ALOGE_IF(mObjectCount != 0, "%s: mObjectCount: %d should be zero on destruction", __func__, mObjectCount.load()); // Design note: it would be possible to keep a map of the outstanding allocated // objects and force a delete on them on ObjectManager destruction. // The consequences of that is probably worse than keeping them alive. } // Retrieves the associated object, returns nullValue T if not available. T get(JNIEnv *env, jobject thiz) { std::lock_guard lg(mLock); // NOLINTNEXTLINE(performance-no-int-to-ptr) auto ptr = reinterpret_cast(env->GetLongField(thiz, mFieldId)); if (ptr != nullptr) { return *ptr; } return {}; } // Sets the object and returns the old one. // // If the old object doesn't exist, then nullValue T is returned. // If the new object is false by operator bool(), the internal object is destroyed. // Note: The old object is returned so if T is a smart pointer, it can be held // by the caller to be deleted outside of any external lock. // // Remember to call set(env, thiz, {}) to destroy the object in the Java // object finalize to avoid orphaned objects on the heap. T set(JNIEnv *env, jobject thiz, const T& newObject) { std::lock_guard lg(mLock); // NOLINTNEXTLINE(performance-no-int-to-ptr) auto ptr = reinterpret_cast(env->GetLongField(thiz, mFieldId)); if (ptr != nullptr) { T old = std::move(*ptr); // *ptr will be replaced or deleted. if (newObject) { env->SetLongField(thiz, mFieldId, (jlong)0); delete ptr; --mObjectCount; } else { *ptr = newObject; } return old; } else { if (newObject) { env->SetLongField(thiz, mFieldId, (jlong)new T(newObject)); ++mObjectCount; } return {}; } } // Returns the number of outstanding objects. // // This is purely for debugging purposes and tracks the number of active Java // objects that have native T objects; hence represents the number of // T heap allocations we have made. // // When all those Java objects have been finalized we expect this to go to 0. int32_t getObjectCount() const { return mObjectCount; } private: // NOLINTNEXTLINE(misc-misplaced-const) const jfieldID mFieldId; // '_jfieldID *const' // mObjectCount is the number of outstanding native T heap allocations we have // made (and thus the number of active Java objects which are associated with them). std::atomic_int32_t mObjectCount{}; mutable std::mutex mLock; }; // We use SoundPoolManager to associate a native std::shared_ptr // object with a field in the Java object. // // We can then retrieve the std::shared_ptr from the object. // // Design notes: // 1) This is based on ObjectManager class. // 2) An alternative that does not require a field in the Java object // is to create an associative map using as a key a NewWeakGlobalRef // to the Java object. // The problem of this method is that lookup is O(N) because comparison // between the WeakGlobalRef to a JNI jobject LocalRef must be done // through the JNI IsSameObject() call, hence iterative through the map. // One advantage of this method is that manual garbage collection // is possible by checking if the WeakGlobalRef is null equivalent. auto& getSoundPoolManager() { static ObjectManager> soundPoolManager(fields.mNativeContext); return soundPoolManager; } inline auto getSoundPool(JNIEnv *env, jobject thiz) { return getSoundPoolManager().get(env, thiz); } // Note: one must call setSoundPool(env, thiz, nullptr) to release any native resources // somewhere in the Java object finalize(). inline auto setSoundPool( JNIEnv *env, jobject thiz, const std::shared_ptr& soundPool) { return getSoundPoolManager().set(env, thiz, soundPool); } /** * ConcurrentHashMap is a locked hash map * * As from the name, this class is thread_safe. * * The type V must have 3 properties in the current implementation. * 1) A V{} default constructor which represents a nullValue. * 2) V::operator bool() const efficient detection of such a nullValue. * 3) V must be copyable. * * Note: The Key cannot be a Java LocalRef, as those change between JNI calls. * The Key could be the raw native object pointer if one wanted to associate * extra data with a native object. * * Using set() with a nullValue V results in erasing the key entry. * A nullValue V is returned by get() if there is no underlying entry. * * Design notes: * 1) For objects of type V that do not naturally have a "nullValue", * wrapping VOpt = std::optional or VShared = std::shared is recommended. * * 2) An overload for an explicit equality comparable nullValue such as * get(..., const V& nullValue) or set(..., const V& nullValue) * is omitted. An alternative is to pass a fixed nullValue into a special * constructor (omitted) for equality comparisons and return value. * * 3) This ConcurrentHashMap currently allows only one thread at a time. * It is not optimized for heavy multi-threaded use. */ template class ConcurrentHashMap { public: // Sets the value and returns the old one. // // If the old value doesn't exist, then nullValue V is returned. // If the new value is false by operator bool(), the internal value is destroyed. // Note: The old value is returned so if V is a smart pointer, it can be held // by the caller to be deleted outside of any external lock. V set(const K& key, const V& value) { std::lock_guard lg(mLock); auto it = mMap.find(key); if (it == mMap.end()) { if (value) { mMap[key] = value; } return {}; } V oldValue = std::move(it->second); if (value) { it->second = value; } else { mMap.erase(it); } return oldValue; } // Retrieves the associated object, returns nullValue V if not available. V get(const K& key) const { std::lock_guard lg(mLock); auto it = mMap.find(key); return it != mMap.end() ? it->second : V{}; } private: mutable std::mutex mLock; std::unordered_map mMap GUARDED_BY(mLock); }; // *jobject is needed to fit the jobject into a std::shared_ptr. // This is the Android type _jobject, but we derive this type as JObjectValue. using JObjectValue = std::remove_pointer_t; // _jobject // Check that jobject is really a pointer to JObjectValue. // The JNI contract is that jobject is NULL comparable, // so jobject is pointer equivalent; we check here to be sure. // Note std::remove_ptr_t == NonPointerType. static_assert(std::is_same_v); // *jweak is needed to fit the jweak into a std::shared_ptr. // This is the Android type _jobject, but we derive this type as JWeakValue. using JWeakValue = std::remove_pointer_t; // this is just _jobject // Check that jweak is really a pointer to JWeakValue. static_assert(std::is_same_v); // We store the ancillary data associated with a SoundPool object in a concurrent // hash map indexed on the SoundPool native object pointer. auto& getSoundPoolJavaRefManager() { // Note this can store shared_ptrs to either jweak and jobject, // as the underlying type is identical. static ConcurrentHashMap> concurrentHashMap; return concurrentHashMap; } // make_shared_globalref_from_localref() creates a sharable Java global // reference from a Java local reference. The equivalent type is // std::shared_ptr<_jobject> (where _jobject is JObjectValue, // and _jobject * is jobject), // and the jobject may be retrieved by .get() or pointer dereference. // This encapsulation gives the benefit of std::shared_ptr // ref counting, weak_ptr, etc. // // The Java global reference should be stable between JNI calls. It is a limited // quantity so sparingly use global references. // // The Android JNI implementation is described here: // https://developer.android.com/training/articles/perf-jni // https://android-developers.googleblog.com/2011/11/jni-local-reference-changes-in-ics.html // // Consider using a weak reference if this is self-referential. [[maybe_unused]] inline auto make_shared_globalref_from_localref(JNIEnv *env, jobject localRef) { return std::shared_ptr( env->NewGlobalRef(localRef), [](JObjectValue* object) { // cannot cache env as don't know which thread we're on. if (object != nullptr) AndroidRuntime::getJNIEnv()->DeleteGlobalRef(object); }); } // Create a weak global reference from local ref. inline auto make_shared_weakglobalref_from_localref(JNIEnv *env, jobject localRef) { return std::shared_ptr( env->NewWeakGlobalRef(localRef), [](JWeakValue* weak) { // cannot cache env as don't know which thread we're on. if (weak != nullptr) AndroidRuntime::getJNIEnv()->DeleteWeakGlobalRef(weak); }); } // std::unique_ptr<> does not store a type-erased deleter like std::shared_ptr<>. // Define a lambda here to use for the std::unique_ptr<> type definition. auto LocalRefDeleter = [](JObjectValue* object) { if (object != nullptr) AndroidRuntime::getJNIEnv()->DeleteLocalRef(object); }; // Create a local reference from another reference. // This is a unique_ptr to avoid the temptation of sharing with other threads. // // This can be used to promote a WeakGlobalRef jweak into a stable LocalRef jobject. // inline auto make_unique_localref_from_ref(JNIEnv *env, jobject object) { return std::unique_ptr( env->NewLocalRef(object), LocalRefDeleter); } } // namespace static const char* const kAudioAttributesClassPathName = "android/media/AudioAttributes"; struct audio_attributes_fields_t { jfieldID fieldUsage; // AudioAttributes.mUsage jfieldID fieldContentType; // AudioAttributes.mContentType jfieldID fieldFlags; // AudioAttributes.mFlags jfieldID fieldFormattedTags;// AudioAttributes.mFormattedTags }; static audio_attributes_fields_t javaAudioAttrFields; // ---------------------------------------------------------------------------- static jint android_media_SoundPool_load_FD(JNIEnv *env, jobject thiz, jobject fileDescriptor, jlong offset, jlong length, jint priority) { ALOGV("android_media_SoundPool_load_FD"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return 0; return (jint) soundPool->load(jniGetFDFromFileDescriptor(env, fileDescriptor), int64_t(offset), int64_t(length), int(priority)); } static jboolean android_media_SoundPool_unload(JNIEnv *env, jobject thiz, jint sampleID) { ALOGV("android_media_SoundPool_unload\n"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return JNI_FALSE; return soundPool->unload(sampleID) ? JNI_TRUE : JNI_FALSE; } static jint android_media_SoundPool_play(JNIEnv *env, jobject thiz, jint sampleID, jfloat leftVolume, jfloat rightVolume, jint priority, jint loop, jfloat rate, jint playerIId) { ALOGV("android_media_SoundPool_play\n"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return 0; return (jint) soundPool->play(sampleID, leftVolume, rightVolume, priority, loop, rate, playerIId); } static void android_media_SoundPool_pause(JNIEnv *env, jobject thiz, jint channelID) { ALOGV("android_media_SoundPool_pause"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return; soundPool->pause(channelID); } static void android_media_SoundPool_resume(JNIEnv *env, jobject thiz, jint channelID) { ALOGV("android_media_SoundPool_resume"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return; soundPool->resume(channelID); } static void android_media_SoundPool_autoPause(JNIEnv *env, jobject thiz) { ALOGV("android_media_SoundPool_autoPause"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return; soundPool->autoPause(); } static void android_media_SoundPool_autoResume(JNIEnv *env, jobject thiz) { ALOGV("android_media_SoundPool_autoResume"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return; soundPool->autoResume(); } static void android_media_SoundPool_stop(JNIEnv *env, jobject thiz, jint channelID) { ALOGV("android_media_SoundPool_stop"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return; soundPool->stop(channelID); } static void android_media_SoundPool_setVolume(JNIEnv *env, jobject thiz, jint channelID, jfloat leftVolume, jfloat rightVolume) { ALOGV("android_media_SoundPool_setVolume"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return; soundPool->setVolume(channelID, (float) leftVolume, (float) rightVolume); } static void android_media_SoundPool_mute(JNIEnv *env, jobject thiz, jboolean muting) { ALOGV("android_media_SoundPool_mute(%d)", muting); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return; soundPool->mute(muting == JNI_TRUE); } static void android_media_SoundPool_setPriority(JNIEnv *env, jobject thiz, jint channelID, jint priority) { ALOGV("android_media_SoundPool_setPriority"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return; soundPool->setPriority(channelID, (int) priority); } static void android_media_SoundPool_setLoop(JNIEnv *env, jobject thiz, jint channelID, int loop) { ALOGV("android_media_SoundPool_setLoop"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return; soundPool->setLoop(channelID, loop); } static void android_media_SoundPool_setRate(JNIEnv *env, jobject thiz, jint channelID, jfloat rate) { ALOGV("android_media_SoundPool_setRate"); auto soundPool = getSoundPool(env, thiz); if (soundPool == nullptr) return; soundPool->setRate(channelID, (float) rate); } static void android_media_callback(SoundPoolEvent event, SoundPool* soundPool, void* user) { ALOGV("callback: (%d, %d, %d, %p, %p)", event.mMsg, event.mArg1, event.mArg2, soundPool, user); auto weakRef = getSoundPoolJavaRefManager().get(soundPool); // shared_ptr to WeakRef if (weakRef == nullptr) { ALOGD("%s: no weak ref, object released, ignoring callback", __func__); return; } JNIEnv *env = AndroidRuntime::getJNIEnv(); // "promote" the WeakGlobalRef into a LocalRef. auto javaSoundPool = make_unique_localref_from_ref(env, weakRef.get()); if (!javaSoundPool) { ALOGW("%s: weak reference promotes to null (release() not called?), " "ignoring callback", __func__); return; } env->CallVoidMethod(javaSoundPool.get(), fields.mPostEvent, event.mMsg, event.mArg1, event.mArg2, nullptr /* object */); if (env->ExceptionCheck() != JNI_FALSE) { ALOGE("%s: Uncaught exception returned from Java callback", __func__); env->ExceptionDescribe(); env->ExceptionClear(); // Just clear it, hopefully all is ok. } } static jint android_media_SoundPool_native_setup(JNIEnv *env, jobject thiz, jint maxChannels, jobject jaa, jstring opPackageName) { ALOGV("android_media_SoundPool_native_setup"); if (jaa == nullptr) { ALOGE("Error creating SoundPool: invalid audio attributes"); return -1; } // Use the AUDIO_ATTRIBUTES_INITIALIZER here to ensure all non-relevant fields are // initialized properly. (note that .source is not explicitly initialized here). audio_attributes_t audioAttributes = AUDIO_ATTRIBUTES_INITIALIZER; // read the AudioAttributes values const auto jtags = (jstring) env->GetObjectField(jaa, javaAudioAttrFields.fieldFormattedTags); const char* tags = env->GetStringUTFChars(jtags, nullptr); // infers array size and guarantees zero termination (does not zero fill to the end). audio_utils_strlcpy(audioAttributes.tags, tags); env->ReleaseStringUTFChars(jtags, tags); audioAttributes.usage = (audio_usage_t) env->GetIntField(jaa, javaAudioAttrFields.fieldUsage); audioAttributes.content_type = (audio_content_type_t) env->GetIntField(jaa, javaAudioAttrFields.fieldContentType); audioAttributes.flags = (audio_flags_mask_t) env->GetIntField(jaa, javaAudioAttrFields.fieldFlags); ScopedUtfChars opPackageNameStr(env, opPackageName); auto soundPool = std::make_shared( maxChannels, audioAttributes, opPackageNameStr.c_str()); soundPool->setCallback(android_media_callback, nullptr /* user */); // register with SoundPoolManager. auto oldSoundPool = setSoundPool(env, thiz, soundPool); // register Java SoundPool WeakRef using native SoundPool * as the key, for the callback. auto oldSoundPoolJavaRef = getSoundPoolJavaRefManager().set( soundPool.get(), make_shared_weakglobalref_from_localref(env, thiz)); ALOGW_IF(oldSoundPool != nullptr, "%s: Aliased SoundPool object %p", __func__, oldSoundPool.get()); return 0; } static void android_media_SoundPool_release(JNIEnv *env, jobject thiz) { ALOGV("android_media_SoundPool_release"); // Remove us from SoundPoolManager. auto oldSoundPool = setSoundPool(env, thiz, nullptr); if (oldSoundPool != nullptr) { // Note: setting the weak ref is thread safe in case there is a callback // simultaneously occurring. auto oldSoundPoolJavaRef = getSoundPoolJavaRefManager().set(oldSoundPool.get(), nullptr); } // destructor to oldSoundPool should occur at exit. } // ---------------------------------------------------------------------------- // Dalvik VM type signatures static JNINativeMethod gMethods[] = { { "_load", "(Ljava/io/FileDescriptor;JJI)I", (void *)android_media_SoundPool_load_FD }, { "unload", "(I)Z", (void *)android_media_SoundPool_unload }, { "_play", "(IFFIIFI)I", (void *)android_media_SoundPool_play }, { "pause", "(I)V", (void *)android_media_SoundPool_pause }, { "resume", "(I)V", (void *)android_media_SoundPool_resume }, { "autoPause", "()V", (void *)android_media_SoundPool_autoPause }, { "autoResume", "()V", (void *)android_media_SoundPool_autoResume }, { "stop", "(I)V", (void *)android_media_SoundPool_stop }, { "_setVolume", "(IFF)V", (void *)android_media_SoundPool_setVolume }, { "_mute", "(Z)V", (void *)android_media_SoundPool_mute }, { "setPriority", "(II)V", (void *)android_media_SoundPool_setPriority }, { "setLoop", "(II)V", (void *)android_media_SoundPool_setLoop }, { "setRate", "(IF)V", (void *)android_media_SoundPool_setRate }, { "native_setup", "(ILjava/lang/Object;Ljava/lang/String;)I", (void*)android_media_SoundPool_native_setup }, { "native_release", "()V", (void*)android_media_SoundPool_release } }; static const char* const kClassPathName = "android/media/SoundPool"; jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) { JNIEnv* env = nullptr; jint result = -1; jclass clazz; if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { ALOGE("ERROR: GetEnv failed\n"); return result; } assert(env != nullptr); clazz = env->FindClass(kClassPathName); if (clazz == nullptr) { ALOGE("Can't find %s", kClassPathName); return result; } fields.mNativeContext = env->GetFieldID(clazz, "mNativeContext", "J"); if (fields.mNativeContext == nullptr) { ALOGE("Can't find SoundPool.mNativeContext"); return result; } fields.mPostEvent = env->GetMethodID( clazz, "postEventFromNative", "(IIILjava/lang/Object;)V"); if (fields.mPostEvent == nullptr) { ALOGE("Can't find android/media/SoundPool.postEventFromNative"); return result; } // create a reference to class. Technically, we're leaking this reference // since it's a static object. fields.mSoundPoolClass = (jclass) env->NewGlobalRef(clazz); if (AndroidRuntime::registerNativeMethods( env, kClassPathName, gMethods, NELEM(gMethods)) < 0) { return result; } // Get the AudioAttributes class and fields jclass audioAttrClass = env->FindClass(kAudioAttributesClassPathName); if (audioAttrClass == nullptr) { ALOGE("Can't find %s", kAudioAttributesClassPathName); return result; } auto audioAttributesClassRef = (jclass)env->NewGlobalRef(audioAttrClass); javaAudioAttrFields.fieldUsage = env->GetFieldID(audioAttributesClassRef, "mUsage", "I"); javaAudioAttrFields.fieldContentType = env->GetFieldID(audioAttributesClassRef, "mContentType", "I"); javaAudioAttrFields.fieldFlags = env->GetFieldID(audioAttributesClassRef, "mFlags", "I"); javaAudioAttrFields.fieldFormattedTags = env->GetFieldID(audioAttributesClassRef, "mFormattedTags", "Ljava/lang/String;"); env->DeleteGlobalRef(audioAttributesClassRef); if (javaAudioAttrFields.fieldUsage == nullptr || javaAudioAttrFields.fieldContentType == nullptr || javaAudioAttrFields.fieldFlags == nullptr || javaAudioAttrFields.fieldFormattedTags == nullptr) { ALOGE("Can't initialize AudioAttributes fields"); return result; } /* success -- return valid version number */ return JNI_VERSION_1_4; }