| /* |
| * Copyright (C) 2022 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 <algorithm> |
| #include <condition_variable> |
| #include <limits> |
| #include <memory> |
| #include <mutex> |
| #include <optional> |
| #include <set> |
| #include <string> |
| #include <vector> |
| |
| #define LOG_TAG "VtsHalAudioCore" |
| #include <android-base/logging.h> |
| |
| #include <StreamWorker.h> |
| #include <aidl/Gtest.h> |
| #include <aidl/Vintf.h> |
| #include <aidl/android/hardware/audio/core/IConfig.h> |
| #include <aidl/android/hardware/audio/core/IModule.h> |
| #include <aidl/android/media/audio/common/AudioIoFlags.h> |
| #include <aidl/android/media/audio/common/AudioOutputFlags.h> |
| #include <android-base/chrono_utils.h> |
| #include <android-base/properties.h> |
| #include <android/binder_manager.h> |
| #include <android/binder_process.h> |
| #include <fmq/AidlMessageQueue.h> |
| |
| #include "ModuleConfig.h" |
| |
| using namespace android; |
| using aidl::android::hardware::audio::common::PlaybackTrackMetadata; |
| using aidl::android::hardware::audio::common::RecordTrackMetadata; |
| using aidl::android::hardware::audio::common::SinkMetadata; |
| using aidl::android::hardware::audio::common::SourceMetadata; |
| using aidl::android::hardware::audio::core::AudioPatch; |
| using aidl::android::hardware::audio::core::AudioRoute; |
| using aidl::android::hardware::audio::core::IModule; |
| using aidl::android::hardware::audio::core::IStreamIn; |
| using aidl::android::hardware::audio::core::IStreamOut; |
| using aidl::android::hardware::audio::core::ModuleDebug; |
| using aidl::android::hardware::audio::core::StreamDescriptor; |
| using aidl::android::hardware::common::fmq::SynchronizedReadWrite; |
| using aidl::android::media::audio::common::AudioContentType; |
| using aidl::android::media::audio::common::AudioDevice; |
| using aidl::android::media::audio::common::AudioDeviceAddress; |
| using aidl::android::media::audio::common::AudioDeviceType; |
| using aidl::android::media::audio::common::AudioFormatType; |
| using aidl::android::media::audio::common::AudioIoFlags; |
| using aidl::android::media::audio::common::AudioOutputFlags; |
| using aidl::android::media::audio::common::AudioPort; |
| using aidl::android::media::audio::common::AudioPortConfig; |
| using aidl::android::media::audio::common::AudioPortDeviceExt; |
| using aidl::android::media::audio::common::AudioPortExt; |
| using aidl::android::media::audio::common::AudioSource; |
| using aidl::android::media::audio::common::AudioUsage; |
| using android::hardware::audio::common::StreamLogic; |
| using android::hardware::audio::common::StreamWorker; |
| using ndk::ScopedAStatus; |
| |
| namespace ndk { |
| std::ostream& operator<<(std::ostream& str, const ScopedAStatus& status) { |
| str << status.getDescription(); |
| return str; |
| } |
| } // namespace ndk |
| |
| template <typename T> |
| auto findById(std::vector<T>& v, int32_t id) { |
| return std::find_if(v.begin(), v.end(), [&](const auto& e) { return e.id == id; }); |
| } |
| |
| template <typename C> |
| std::vector<int32_t> GetNonExistentIds(const C& allIds) { |
| if (allIds.empty()) { |
| return std::vector<int32_t>{-1, 0, 1}; |
| } |
| std::vector<int32_t> nonExistentIds; |
| nonExistentIds.push_back(*std::min_element(allIds.begin(), allIds.end()) - 1); |
| nonExistentIds.push_back(*std::max_element(allIds.begin(), allIds.end()) + 1); |
| return nonExistentIds; |
| } |
| |
| AudioDeviceAddress GenerateUniqueDeviceAddress() { |
| static int nextId = 1; |
| // TODO: Use connection-specific ID. |
| return AudioDeviceAddress::make<AudioDeviceAddress::Tag::id>(std::to_string(++nextId)); |
| } |
| |
| struct AidlDeathRecipient { |
| const ndk::SpAIBinder binder; |
| const ndk::ScopedAIBinder_DeathRecipient recipient; |
| std::mutex mutex; |
| std::condition_variable condition; |
| bool fired = false; |
| |
| explicit AidlDeathRecipient(const ndk::SpAIBinder& binder) |
| : binder(binder), recipient(AIBinder_DeathRecipient_new(&binderDiedCallbackAidl)) {} |
| |
| binder_status_t linkToDeath() { |
| return AIBinder_linkToDeath(binder.get(), recipient.get(), this); |
| } |
| |
| void binderDied() { |
| std::unique_lock<std::mutex> lock(mutex); |
| fired = true; |
| condition.notify_one(); |
| }; |
| |
| bool waitForFired(int timeoutMs) { |
| std::unique_lock<std::mutex> lock(mutex); |
| condition.wait_for(lock, std::chrono::milliseconds(timeoutMs), [this]() { return fired; }); |
| return fired; |
| } |
| |
| static void binderDiedCallbackAidl(void* cookie) { |
| AidlDeathRecipient* self = static_cast<AidlDeathRecipient*>(cookie); |
| self->binderDied(); |
| } |
| }; |
| |
| // All 'With*' classes are move-only because they are associated with some |
| // resource or state of a HAL module. |
| class WithDebugFlags { |
| public: |
| static WithDebugFlags createNested(const WithDebugFlags& parent) { |
| return WithDebugFlags(parent.mFlags); |
| } |
| |
| WithDebugFlags() {} |
| explicit WithDebugFlags(const ModuleDebug& initial) : mInitial(initial), mFlags(initial) {} |
| WithDebugFlags(const WithDebugFlags&) = delete; |
| WithDebugFlags& operator=(const WithDebugFlags&) = delete; |
| ~WithDebugFlags() { |
| if (mModule != nullptr) { |
| ScopedAStatus status = mModule->setModuleDebug(mInitial); |
| EXPECT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| } |
| void SetUp(IModule* module) { |
| ScopedAStatus status = module->setModuleDebug(mFlags); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| ModuleDebug& flags() { return mFlags; } |
| |
| private: |
| ModuleDebug mInitial; |
| ModuleDebug mFlags; |
| IModule* mModule = nullptr; |
| }; |
| |
| // For consistency, WithAudioPortConfig can start both with a non-existent |
| // port config, and with an existing one. Existence is determined by the |
| // id of the provided config. If it's not 0, then WithAudioPortConfig is |
| // essentially a no-op wrapper. |
| class WithAudioPortConfig { |
| public: |
| WithAudioPortConfig() {} |
| explicit WithAudioPortConfig(const AudioPortConfig& config) : mInitialConfig(config) {} |
| WithAudioPortConfig(const WithAudioPortConfig&) = delete; |
| WithAudioPortConfig& operator=(const WithAudioPortConfig&) = delete; |
| ~WithAudioPortConfig() { |
| if (mModule != nullptr) { |
| ScopedAStatus status = mModule->resetAudioPortConfig(getId()); |
| EXPECT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << "; port config id " << getId(); |
| } |
| } |
| void SetUp(IModule* module) { |
| ASSERT_NE(AudioPortExt::Tag::unspecified, mInitialConfig.ext.getTag()) |
| << "config: " << mInitialConfig.toString(); |
| // Negotiation is allowed for device ports because the HAL module is |
| // allowed to provide an empty profiles list for attached devices. |
| ASSERT_NO_FATAL_FAILURE( |
| SetUpImpl(module, mInitialConfig.ext.getTag() == AudioPortExt::Tag::device)); |
| } |
| int32_t getId() const { return mConfig.id; } |
| const AudioPortConfig& get() const { return mConfig; } |
| |
| private: |
| void SetUpImpl(IModule* module, bool negotiate) { |
| if (mInitialConfig.id == 0) { |
| AudioPortConfig suggested; |
| bool applied = false; |
| ScopedAStatus status = module->setAudioPortConfig(mInitialConfig, &suggested, &applied); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << "; Config: " << mInitialConfig.toString(); |
| if (!applied && negotiate) { |
| mInitialConfig = suggested; |
| ASSERT_NO_FATAL_FAILURE(SetUpImpl(module, false)) |
| << " while applying suggested config: " << suggested.toString(); |
| } else { |
| ASSERT_TRUE(applied) << "Suggested: " << suggested.toString(); |
| mConfig = suggested; |
| mModule = module; |
| } |
| } else { |
| mConfig = mInitialConfig; |
| } |
| } |
| |
| AudioPortConfig mInitialConfig; |
| IModule* mModule = nullptr; |
| AudioPortConfig mConfig; |
| }; |
| |
| class AudioCoreModule : public testing::TestWithParam<std::string> { |
| public: |
| // The default buffer size is used mostly for negative tests. |
| static constexpr int kDefaultBufferSizeFrames = 256; |
| |
| void SetUp() override { |
| ASSERT_NO_FATAL_FAILURE(ConnectToService()); |
| debug.flags().simulateDeviceConnections = true; |
| ASSERT_NO_FATAL_FAILURE(debug.SetUp(module.get())); |
| } |
| |
| void TearDown() override { |
| if (module != nullptr) { |
| ScopedAStatus status = module->setModuleDebug(ModuleDebug{}); |
| EXPECT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << " returned when resetting debug flags"; |
| } |
| } |
| |
| void ConnectToService() { |
| module = IModule::fromBinder( |
| ndk::SpAIBinder(AServiceManager_getService(GetParam().c_str()))); |
| ASSERT_NE(module, nullptr); |
| } |
| |
| void RestartService() { |
| ASSERT_NE(module, nullptr); |
| moduleConfig.reset(); |
| deathHandler.reset(new AidlDeathRecipient(module->asBinder())); |
| ASSERT_EQ(STATUS_OK, deathHandler->linkToDeath()); |
| ASSERT_TRUE(base::SetProperty("sys.audio.restart.hal", "1")); |
| EXPECT_TRUE(deathHandler->waitForFired(3000)); |
| deathHandler.reset(); |
| ASSERT_NO_FATAL_FAILURE(ConnectToService()); |
| } |
| |
| void ApplyEveryConfig(const std::vector<AudioPortConfig>& configs) { |
| for (const auto& config : configs) { |
| ASSERT_NE(0, config.portId); |
| WithAudioPortConfig portConfig(config); |
| ASSERT_NO_FATAL_FAILURE(portConfig.SetUp(module.get())); // calls setAudioPortConfig |
| EXPECT_EQ(config.portId, portConfig.get().portId); |
| std::vector<AudioPortConfig> retrievedPortConfigs; |
| ScopedAStatus status = module->getAudioPortConfigs(&retrievedPortConfigs); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| const int32_t portConfigId = portConfig.getId(); |
| auto configIt = std::find_if( |
| retrievedPortConfigs.begin(), retrievedPortConfigs.end(), |
| [&portConfigId](const auto& retrConf) { return retrConf.id == portConfigId; }); |
| EXPECT_NE(configIt, retrievedPortConfigs.end()) |
| << "Port config id returned by setAudioPortConfig: " << portConfigId |
| << " is not found in the list returned by getAudioPortConfigs"; |
| if (configIt != retrievedPortConfigs.end()) { |
| EXPECT_EQ(portConfig.get(), *configIt) |
| << "Applied port config returned by setAudioPortConfig: " |
| << portConfig.get().toString() |
| << " is not the same as retrieved via getAudioPortConfigs: " |
| << configIt->toString(); |
| } |
| } |
| } |
| |
| template <typename Entity> |
| void GetAllEntityIds(std::set<int32_t>* entityIds, |
| ScopedAStatus (IModule::*getter)(std::vector<Entity>*), |
| const std::string& errorMessage) { |
| std::vector<Entity> entities; |
| { |
| ScopedAStatus status = (module.get()->*getter)(&entities); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| std::transform(entities.begin(), entities.end(), |
| std::inserter(*entityIds, entityIds->begin()), |
| [](const auto& entity) { return entity.id; }); |
| EXPECT_EQ(entities.size(), entityIds->size()) << errorMessage; |
| } |
| |
| void GetAllPatchIds(std::set<int32_t>* patchIds) { |
| return GetAllEntityIds<AudioPatch>( |
| patchIds, &IModule::getAudioPatches, |
| "IDs of audio patches returned by IModule.getAudioPatches are not unique"); |
| } |
| |
| void GetAllPortIds(std::set<int32_t>* portIds) { |
| return GetAllEntityIds<AudioPort>( |
| portIds, &IModule::getAudioPorts, |
| "IDs of audio ports returned by IModule.getAudioPorts are not unique"); |
| } |
| |
| void GetAllPortConfigIds(std::set<int32_t>* portConfigIds) { |
| return GetAllEntityIds<AudioPortConfig>( |
| portConfigIds, &IModule::getAudioPortConfigs, |
| "IDs of audio port configs returned by IModule.getAudioPortConfigs are not unique"); |
| } |
| |
| void SetUpModuleConfig() { |
| if (moduleConfig == nullptr) { |
| moduleConfig = std::make_unique<ModuleConfig>(module.get()); |
| ASSERT_EQ(EX_NONE, moduleConfig->getStatus().getExceptionCode()) |
| << "ModuleConfig init error: " << moduleConfig->getError(); |
| } |
| } |
| |
| std::shared_ptr<IModule> module; |
| std::unique_ptr<AidlDeathRecipient> deathHandler; |
| std::unique_ptr<ModuleConfig> moduleConfig; |
| WithDebugFlags debug; |
| }; |
| |
| class WithDevicePortConnectedState { |
| public: |
| explicit WithDevicePortConnectedState(const AudioPort& idAndData) : mIdAndData(idAndData) {} |
| WithDevicePortConnectedState(const AudioPort& id, const AudioDeviceAddress& address) |
| : mIdAndData(setAudioPortAddress(id, address)) {} |
| WithDevicePortConnectedState(const WithDevicePortConnectedState&) = delete; |
| WithDevicePortConnectedState& operator=(const WithDevicePortConnectedState&) = delete; |
| ~WithDevicePortConnectedState() { |
| if (mModule != nullptr) { |
| ScopedAStatus status = mModule->disconnectExternalDevice(getId()); |
| EXPECT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << " returned when disconnecting device port ID " << getId(); |
| } |
| } |
| void SetUp(IModule* module) { |
| ScopedAStatus status = module->connectExternalDevice(mIdAndData, &mConnectedPort); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << " returned when connecting device port ID & data " |
| << mIdAndData.toString(); |
| ASSERT_NE(mIdAndData.id, getId()) |
| << "ID of the connected port must not be the same as the ID of the template port"; |
| mModule = module; |
| } |
| int32_t getId() const { return mConnectedPort.id; } |
| const AudioPort& get() { return mConnectedPort; } |
| |
| private: |
| static AudioPort setAudioPortAddress(const AudioPort& id, const AudioDeviceAddress& address) { |
| AudioPort result = id; |
| result.ext.get<AudioPortExt::Tag::device>().device.address = address; |
| return result; |
| } |
| |
| const AudioPort mIdAndData; |
| IModule* mModule = nullptr; |
| AudioPort mConnectedPort; |
| }; |
| |
| class StreamContext { |
| public: |
| typedef AidlMessageQueue<StreamDescriptor::Command, SynchronizedReadWrite> CommandMQ; |
| typedef AidlMessageQueue<StreamDescriptor::Reply, SynchronizedReadWrite> ReplyMQ; |
| typedef AidlMessageQueue<int8_t, SynchronizedReadWrite> DataMQ; |
| |
| explicit StreamContext(const StreamDescriptor& descriptor) |
| : mFrameSizeBytes(descriptor.frameSizeBytes), |
| mCommandMQ(new CommandMQ(descriptor.command)), |
| mReplyMQ(new ReplyMQ(descriptor.reply)), |
| mBufferSizeFrames(descriptor.bufferSizeFrames), |
| mDataMQ(maybeCreateDataMQ(descriptor)) {} |
| void checkIsValid() const { |
| EXPECT_NE(0UL, mFrameSizeBytes); |
| ASSERT_NE(nullptr, mCommandMQ); |
| EXPECT_TRUE(mCommandMQ->isValid()); |
| ASSERT_NE(nullptr, mReplyMQ); |
| EXPECT_TRUE(mReplyMQ->isValid()); |
| if (mDataMQ != nullptr) { |
| EXPECT_TRUE(mDataMQ->isValid()); |
| EXPECT_GE(mDataMQ->getQuantumCount() * mDataMQ->getQuantumSize(), |
| mFrameSizeBytes * mBufferSizeFrames) |
| << "Data MQ actual buffer size is " |
| "less than the buffer size as specified by the descriptor"; |
| } |
| } |
| size_t getBufferSizeBytes() const { return mFrameSizeBytes * mBufferSizeFrames; } |
| size_t getBufferSizeFrames() const { return mBufferSizeFrames; } |
| CommandMQ* getCommandMQ() const { return mCommandMQ.get(); } |
| DataMQ* getDataMQ() const { return mDataMQ.get(); } |
| ReplyMQ* getReplyMQ() const { return mReplyMQ.get(); } |
| |
| private: |
| static std::unique_ptr<DataMQ> maybeCreateDataMQ(const StreamDescriptor& descriptor) { |
| using Tag = StreamDescriptor::AudioBuffer::Tag; |
| if (descriptor.audio.getTag() == Tag::fmq) { |
| return std::make_unique<DataMQ>(descriptor.audio.get<Tag::fmq>()); |
| } |
| return nullptr; |
| } |
| |
| const size_t mFrameSizeBytes; |
| std::unique_ptr<CommandMQ> mCommandMQ; |
| std::unique_ptr<ReplyMQ> mReplyMQ; |
| const size_t mBufferSizeFrames; |
| std::unique_ptr<DataMQ> mDataMQ; |
| }; |
| |
| class StreamCommonLogic : public StreamLogic { |
| public: |
| StreamDescriptor::Position getLastObservablePosition() { |
| std::lock_guard<std::mutex> lock(mLock); |
| return mLastReply.observable; |
| } |
| |
| protected: |
| explicit StreamCommonLogic(const StreamContext& context) |
| : mCommandMQ(context.getCommandMQ()), |
| mReplyMQ(context.getReplyMQ()), |
| mDataMQ(context.getDataMQ()), |
| mData(context.getBufferSizeBytes()) {} |
| StreamContext::CommandMQ* getCommandMQ() const { return mCommandMQ; } |
| StreamContext::ReplyMQ* getReplyMQ() const { return mReplyMQ; } |
| |
| std::string init() override { return ""; } |
| |
| StreamContext::CommandMQ* mCommandMQ; |
| StreamContext::ReplyMQ* mReplyMQ; |
| StreamContext::DataMQ* mDataMQ; |
| std::vector<int8_t> mData; |
| std::mutex mLock; |
| StreamDescriptor::Reply mLastReply GUARDED_BY(mLock); |
| }; |
| |
| class StreamReaderLogic : public StreamCommonLogic { |
| public: |
| explicit StreamReaderLogic(const StreamContext& context) : StreamCommonLogic(context) {} |
| |
| protected: |
| Status cycle() override { |
| StreamDescriptor::Command command{}; |
| command.code = StreamDescriptor::COMMAND_BURST; |
| command.fmqByteCount = mData.size(); |
| if (!mCommandMQ->writeBlocking(&command, 1)) { |
| LOG(ERROR) << __func__ << ": writing of command into MQ failed"; |
| return Status::ABORT; |
| } |
| StreamDescriptor::Reply reply{}; |
| if (!mReplyMQ->readBlocking(&reply, 1)) { |
| LOG(ERROR) << __func__ << ": reading of reply from MQ failed"; |
| return Status::ABORT; |
| } |
| if (reply.status != STATUS_OK) { |
| LOG(ERROR) << __func__ << ": received error status: " << statusToString(reply.status); |
| return Status::ABORT; |
| } |
| if (reply.fmqByteCount < 0 || reply.fmqByteCount > command.fmqByteCount) { |
| LOG(ERROR) << __func__ |
| << ": received invalid byte count in the reply: " << reply.fmqByteCount; |
| return Status::ABORT; |
| } |
| { |
| std::lock_guard<std::mutex> lock(mLock); |
| mLastReply = reply; |
| } |
| const size_t readCount = std::min({mDataMQ->availableToRead(), |
| static_cast<size_t>(reply.fmqByteCount), mData.size()}); |
| if (readCount == 0 || mDataMQ->read(mData.data(), readCount)) { |
| return Status::CONTINUE; |
| } |
| LOG(ERROR) << __func__ << ": reading of " << readCount << " data bytes from MQ failed"; |
| return Status::ABORT; |
| } |
| }; |
| using StreamReader = StreamWorker<StreamReaderLogic>; |
| |
| class StreamWriterLogic : public StreamCommonLogic { |
| public: |
| explicit StreamWriterLogic(const StreamContext& context) : StreamCommonLogic(context) {} |
| |
| protected: |
| Status cycle() override { |
| if (!mDataMQ->write(mData.data(), mData.size())) { |
| LOG(ERROR) << __func__ << ": writing of " << mData.size() << " bytes to MQ failed"; |
| return Status::ABORT; |
| } |
| StreamDescriptor::Command command{}; |
| command.code = StreamDescriptor::COMMAND_BURST; |
| command.fmqByteCount = mData.size(); |
| if (!mCommandMQ->writeBlocking(&command, 1)) { |
| LOG(ERROR) << __func__ << ": writing of command into MQ failed"; |
| return Status::ABORT; |
| } |
| StreamDescriptor::Reply reply{}; |
| if (!mReplyMQ->readBlocking(&reply, 1)) { |
| LOG(ERROR) << __func__ << ": reading of reply from MQ failed"; |
| return Status::ABORT; |
| } |
| if (reply.status != STATUS_OK) { |
| LOG(ERROR) << __func__ << ": received error status: " << statusToString(reply.status); |
| return Status::ABORT; |
| } |
| if (reply.fmqByteCount < 0 || reply.fmqByteCount > command.fmqByteCount) { |
| LOG(ERROR) << __func__ |
| << ": received invalid byte count in the reply: " << reply.fmqByteCount; |
| return Status::ABORT; |
| } |
| { |
| std::lock_guard<std::mutex> lock(mLock); |
| mLastReply = reply; |
| } |
| return Status::CONTINUE; |
| } |
| }; |
| using StreamWriter = StreamWorker<StreamWriterLogic>; |
| |
| template <typename T> |
| struct IOTraits { |
| static constexpr bool is_input = std::is_same_v<T, IStreamIn>; |
| using Worker = std::conditional_t<is_input, StreamReader, StreamWriter>; |
| }; |
| |
| // A dedicated version to test replies to invalid commands. |
| class StreamInvalidCommandLogic : public StreamCommonLogic { |
| public: |
| StreamInvalidCommandLogic(const StreamContext& context, |
| const std::vector<StreamDescriptor::Command>& commands) |
| : StreamCommonLogic(context), mCommands(commands) {} |
| |
| std::vector<std::string> getUnexpectedStatuses() { |
| std::lock_guard<std::mutex> lock(mLock); |
| return mUnexpectedStatuses; |
| } |
| |
| protected: |
| Status cycle() override { |
| // Send all commands in one cycle to simplify testing. |
| // Extra logging helps to sort out issues with unexpected HAL behavior. |
| for (const auto& command : mCommands) { |
| LOG(INFO) << __func__ << ": writing command " << command.toString() << " into MQ..."; |
| if (!getCommandMQ()->writeBlocking(&command, 1)) { |
| LOG(ERROR) << __func__ << ": writing of command into MQ failed"; |
| return Status::ABORT; |
| } |
| StreamDescriptor::Reply reply{}; |
| LOG(INFO) << __func__ << ": reading reply for command " << command.toString() << "..."; |
| if (!getReplyMQ()->readBlocking(&reply, 1)) { |
| LOG(ERROR) << __func__ << ": reading of reply from MQ failed"; |
| return Status::ABORT; |
| } |
| LOG(INFO) << __func__ << ": received status " << statusToString(reply.status) |
| << " for command " << command.toString(); |
| if (reply.status != STATUS_BAD_VALUE) { |
| std::string s = command.toString(); |
| s.append(", ").append(statusToString(reply.status)); |
| std::lock_guard<std::mutex> lock(mLock); |
| mUnexpectedStatuses.push_back(std::move(s)); |
| } |
| }; |
| return Status::EXIT; |
| } |
| |
| private: |
| const std::vector<StreamDescriptor::Command> mCommands; |
| std::mutex mLock; |
| std::vector<std::string> mUnexpectedStatuses GUARDED_BY(mLock); |
| }; |
| |
| template <typename Stream> |
| class WithStream { |
| public: |
| WithStream() {} |
| explicit WithStream(const AudioPortConfig& portConfig) : mPortConfig(portConfig) {} |
| WithStream(const WithStream&) = delete; |
| WithStream& operator=(const WithStream&) = delete; |
| ~WithStream() { |
| if (mStream != nullptr) { |
| mContext.reset(); |
| ScopedAStatus status = mStream->close(); |
| EXPECT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << "; port config id " << getPortId(); |
| } |
| } |
| void SetUpPortConfig(IModule* module) { ASSERT_NO_FATAL_FAILURE(mPortConfig.SetUp(module)); } |
| ScopedAStatus SetUpNoChecks(IModule* module, long bufferSizeFrames) { |
| return SetUpNoChecks(module, mPortConfig.get(), bufferSizeFrames); |
| } |
| ScopedAStatus SetUpNoChecks(IModule* module, const AudioPortConfig& portConfig, |
| long bufferSizeFrames); |
| void SetUp(IModule* module, long bufferSizeFrames) { |
| ASSERT_NO_FATAL_FAILURE(SetUpPortConfig(module)); |
| ScopedAStatus status = SetUpNoChecks(module, bufferSizeFrames); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << "; port config id " << getPortId(); |
| ASSERT_NE(nullptr, mStream) << "; port config id " << getPortId(); |
| EXPECT_GE(mDescriptor.bufferSizeFrames, bufferSizeFrames) |
| << "actual buffer size must be no less than requested"; |
| mContext.emplace(mDescriptor); |
| ASSERT_NO_FATAL_FAILURE(mContext.value().checkIsValid()); |
| } |
| Stream* get() const { return mStream.get(); } |
| const StreamContext* getContext() const { return mContext ? &(mContext.value()) : nullptr; } |
| std::shared_ptr<Stream> getSharedPointer() const { return mStream; } |
| const AudioPortConfig& getPortConfig() const { return mPortConfig.get(); } |
| int32_t getPortId() const { return mPortConfig.getId(); } |
| |
| private: |
| WithAudioPortConfig mPortConfig; |
| std::shared_ptr<Stream> mStream; |
| StreamDescriptor mDescriptor; |
| std::optional<StreamContext> mContext; |
| }; |
| |
| SinkMetadata GenerateSinkMetadata(const AudioPortConfig& portConfig) { |
| RecordTrackMetadata trackMeta; |
| trackMeta.source = AudioSource::MIC; |
| trackMeta.gain = 1.0; |
| trackMeta.channelMask = portConfig.channelMask.value(); |
| SinkMetadata metadata; |
| metadata.tracks.push_back(trackMeta); |
| return metadata; |
| } |
| |
| template <> |
| ScopedAStatus WithStream<IStreamIn>::SetUpNoChecks(IModule* module, |
| const AudioPortConfig& portConfig, |
| long bufferSizeFrames) { |
| aidl::android::hardware::audio::core::IModule::OpenInputStreamArguments args; |
| args.portConfigId = portConfig.id; |
| args.sinkMetadata = GenerateSinkMetadata(portConfig); |
| args.bufferSizeFrames = bufferSizeFrames; |
| aidl::android::hardware::audio::core::IModule::OpenInputStreamReturn ret; |
| ScopedAStatus status = module->openInputStream(args, &ret); |
| if (status.isOk()) { |
| mStream = std::move(ret.stream); |
| mDescriptor = std::move(ret.desc); |
| } |
| return status; |
| } |
| |
| SourceMetadata GenerateSourceMetadata(const AudioPortConfig& portConfig) { |
| PlaybackTrackMetadata trackMeta; |
| trackMeta.usage = AudioUsage::MEDIA; |
| trackMeta.contentType = AudioContentType::MUSIC; |
| trackMeta.gain = 1.0; |
| trackMeta.channelMask = portConfig.channelMask.value(); |
| SourceMetadata metadata; |
| metadata.tracks.push_back(trackMeta); |
| return metadata; |
| } |
| |
| template <> |
| ScopedAStatus WithStream<IStreamOut>::SetUpNoChecks(IModule* module, |
| const AudioPortConfig& portConfig, |
| long bufferSizeFrames) { |
| aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments args; |
| args.portConfigId = portConfig.id; |
| args.sourceMetadata = GenerateSourceMetadata(portConfig); |
| args.offloadInfo = ModuleConfig::generateOffloadInfoIfNeeded(portConfig); |
| args.bufferSizeFrames = bufferSizeFrames; |
| aidl::android::hardware::audio::core::IModule::OpenOutputStreamReturn ret; |
| ScopedAStatus status = module->openOutputStream(args, &ret); |
| if (status.isOk()) { |
| mStream = std::move(ret.stream); |
| mDescriptor = std::move(ret.desc); |
| } |
| return status; |
| } |
| |
| class WithAudioPatch { |
| public: |
| WithAudioPatch() {} |
| WithAudioPatch(const AudioPortConfig& srcPortConfig, const AudioPortConfig& sinkPortConfig) |
| : mSrcPortConfig(srcPortConfig), mSinkPortConfig(sinkPortConfig) {} |
| WithAudioPatch(bool sinkIsCfg1, const AudioPortConfig& portConfig1, |
| const AudioPortConfig& portConfig2) |
| : mSrcPortConfig(sinkIsCfg1 ? portConfig2 : portConfig1), |
| mSinkPortConfig(sinkIsCfg1 ? portConfig1 : portConfig2) {} |
| WithAudioPatch(const WithAudioPatch&) = delete; |
| WithAudioPatch& operator=(const WithAudioPatch&) = delete; |
| ~WithAudioPatch() { |
| if (mModule != nullptr && mPatch.id != 0) { |
| ScopedAStatus status = mModule->resetAudioPatch(mPatch.id); |
| EXPECT_EQ(EX_NONE, status.getExceptionCode()) << status << "; patch id " << getId(); |
| } |
| } |
| void SetUpPortConfigs(IModule* module) { |
| ASSERT_NO_FATAL_FAILURE(mSrcPortConfig.SetUp(module)); |
| ASSERT_NO_FATAL_FAILURE(mSinkPortConfig.SetUp(module)); |
| } |
| ScopedAStatus SetUpNoChecks(IModule* module) { |
| mModule = module; |
| mPatch.sourcePortConfigIds = std::vector<int32_t>{mSrcPortConfig.getId()}; |
| mPatch.sinkPortConfigIds = std::vector<int32_t>{mSinkPortConfig.getId()}; |
| return mModule->setAudioPatch(mPatch, &mPatch); |
| } |
| void SetUp(IModule* module) { |
| ASSERT_NO_FATAL_FAILURE(SetUpPortConfigs(module)); |
| ScopedAStatus status = SetUpNoChecks(module); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << "; source port config id " << mSrcPortConfig.getId() |
| << "; sink port config id " << mSinkPortConfig.getId(); |
| EXPECT_GT(mPatch.minimumStreamBufferSizeFrames, 0) << "patch id " << getId(); |
| for (auto latencyMs : mPatch.latenciesMs) { |
| EXPECT_GT(latencyMs, 0) << "patch id " << getId(); |
| } |
| } |
| int32_t getId() const { return mPatch.id; } |
| const AudioPatch& get() const { return mPatch; } |
| const AudioPortConfig& getSinkPortConfig() const { return mSinkPortConfig.get(); } |
| const AudioPortConfig& getSrcPortConfig() const { return mSrcPortConfig.get(); } |
| const AudioPortConfig& getPortConfig(bool getSink) const { |
| return getSink ? getSinkPortConfig() : getSrcPortConfig(); |
| } |
| |
| private: |
| WithAudioPortConfig mSrcPortConfig; |
| WithAudioPortConfig mSinkPortConfig; |
| IModule* mModule = nullptr; |
| AudioPatch mPatch; |
| }; |
| |
| TEST_P(AudioCoreModule, Published) { |
| // SetUp must complete with no failures. |
| } |
| |
| TEST_P(AudioCoreModule, CanBeRestarted) { |
| ASSERT_NO_FATAL_FAILURE(RestartService()); |
| } |
| |
| TEST_P(AudioCoreModule, PortIdsAreUnique) { |
| std::set<int32_t> portIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); |
| } |
| |
| TEST_P(AudioCoreModule, GetAudioPortsIsStable) { |
| std::vector<AudioPort> ports1; |
| { |
| ScopedAStatus status = module->getAudioPorts(&ports1); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| std::vector<AudioPort> ports2; |
| { |
| ScopedAStatus status = module->getAudioPorts(&ports2); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| ASSERT_EQ(ports1.size(), ports2.size()) |
| << "Sizes of audio port arrays do not match across consequent calls to getAudioPorts"; |
| std::sort(ports1.begin(), ports1.end()); |
| std::sort(ports2.begin(), ports2.end()); |
| EXPECT_EQ(ports1, ports2); |
| } |
| |
| TEST_P(AudioCoreModule, GetAudioRoutesIsStable) { |
| std::vector<AudioRoute> routes1; |
| { |
| ScopedAStatus status = module->getAudioRoutes(&routes1); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| std::vector<AudioRoute> routes2; |
| { |
| ScopedAStatus status = module->getAudioRoutes(&routes2); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| ASSERT_EQ(routes1.size(), routes2.size()) |
| << "Sizes of audio route arrays do not match across consequent calls to getAudioRoutes"; |
| std::sort(routes1.begin(), routes1.end()); |
| std::sort(routes2.begin(), routes2.end()); |
| EXPECT_EQ(routes1, routes2); |
| } |
| |
| TEST_P(AudioCoreModule, GetAudioRoutesAreValid) { |
| std::vector<AudioRoute> routes; |
| { |
| ScopedAStatus status = module->getAudioRoutes(&routes); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| for (const auto& route : routes) { |
| std::set<int32_t> sources(route.sourcePortIds.begin(), route.sourcePortIds.end()); |
| EXPECT_NE(0UL, sources.size()) |
| << "empty audio port sinks in the audio route: " << route.toString(); |
| EXPECT_EQ(sources.size(), route.sourcePortIds.size()) |
| << "IDs of audio port sinks are not unique in the audio route: " |
| << route.toString(); |
| } |
| } |
| |
| TEST_P(AudioCoreModule, GetAudioRoutesPortIdsAreValid) { |
| std::set<int32_t> portIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); |
| std::vector<AudioRoute> routes; |
| { |
| ScopedAStatus status = module->getAudioRoutes(&routes); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| for (const auto& route : routes) { |
| EXPECT_EQ(1UL, portIds.count(route.sinkPortId)) |
| << route.sinkPortId << " sink port id is unknown"; |
| for (const auto& source : route.sourcePortIds) { |
| EXPECT_EQ(1UL, portIds.count(source)) << source << " source port id is unknown"; |
| } |
| } |
| } |
| |
| TEST_P(AudioCoreModule, GetAudioRoutesForAudioPort) { |
| std::set<int32_t> portIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); |
| if (portIds.empty()) { |
| GTEST_SKIP() << "No ports in the module."; |
| } |
| for (const auto portId : portIds) { |
| std::vector<AudioRoute> routes; |
| ScopedAStatus status = module->getAudioRoutesForAudioPort(portId, &routes); |
| EXPECT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| for (const auto& r : routes) { |
| if (r.sinkPortId != portId) { |
| const auto& srcs = r.sourcePortIds; |
| EXPECT_TRUE(std::find(srcs.begin(), srcs.end(), portId) != srcs.end()) |
| << " port ID " << portId << " does not used by the route " << r.toString(); |
| } |
| } |
| } |
| for (const auto portId : GetNonExistentIds(portIds)) { |
| std::vector<AudioRoute> routes; |
| ScopedAStatus status = module->getAudioRoutesForAudioPort(portId, &routes); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for port ID " << portId; |
| } |
| } |
| |
| TEST_P(AudioCoreModule, CheckDevicePorts) { |
| std::vector<AudioPort> ports; |
| { |
| ScopedAStatus status = module->getAudioPorts(&ports); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| std::optional<int32_t> defaultOutput, defaultInput; |
| std::set<AudioDevice> inputs, outputs; |
| const int defaultDeviceFlag = 1 << AudioPortDeviceExt::FLAG_INDEX_DEFAULT_DEVICE; |
| for (const auto& port : ports) { |
| if (port.ext.getTag() != AudioPortExt::Tag::device) continue; |
| const auto& devicePort = port.ext.get<AudioPortExt::Tag::device>(); |
| EXPECT_NE(AudioDeviceType::NONE, devicePort.device.type.type); |
| EXPECT_NE(AudioDeviceType::IN_DEFAULT, devicePort.device.type.type); |
| EXPECT_NE(AudioDeviceType::OUT_DEFAULT, devicePort.device.type.type); |
| if (devicePort.device.type.type > AudioDeviceType::IN_DEFAULT && |
| devicePort.device.type.type < AudioDeviceType::OUT_DEFAULT) { |
| EXPECT_EQ(AudioIoFlags::Tag::input, port.flags.getTag()); |
| } else if (devicePort.device.type.type > AudioDeviceType::OUT_DEFAULT) { |
| EXPECT_EQ(AudioIoFlags::Tag::output, port.flags.getTag()); |
| } |
| EXPECT_FALSE((devicePort.flags & defaultDeviceFlag) != 0 && |
| !devicePort.device.type.connection.empty()) |
| << "Device port " << port.id |
| << " must be permanently attached to be set as default"; |
| if ((devicePort.flags & defaultDeviceFlag) != 0) { |
| if (port.flags.getTag() == AudioIoFlags::Tag::output) { |
| EXPECT_FALSE(defaultOutput.has_value()) |
| << "At least two output device ports are declared as default: " |
| << defaultOutput.value() << " and " << port.id; |
| defaultOutput = port.id; |
| EXPECT_EQ(0UL, outputs.count(devicePort.device)) |
| << "Non-unique output device: " << devicePort.device.toString(); |
| outputs.insert(devicePort.device); |
| } else if (port.flags.getTag() == AudioIoFlags::Tag::input) { |
| EXPECT_FALSE(defaultInput.has_value()) |
| << "At least two input device ports are declared as default: " |
| << defaultInput.value() << " and " << port.id; |
| defaultInput = port.id; |
| EXPECT_EQ(0UL, inputs.count(devicePort.device)) |
| << "Non-unique input device: " << devicePort.device.toString(); |
| inputs.insert(devicePort.device); |
| } else { |
| FAIL() << "Invalid AudioIoFlags Tag: " << toString(port.flags.getTag()); |
| } |
| } |
| } |
| } |
| |
| TEST_P(AudioCoreModule, CheckMixPorts) { |
| std::vector<AudioPort> ports; |
| { |
| ScopedAStatus status = module->getAudioPorts(&ports); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| std::optional<int32_t> primaryMixPort; |
| constexpr int primaryOutputFlag = 1 << static_cast<int>(AudioOutputFlags::PRIMARY); |
| for (const auto& port : ports) { |
| if (port.ext.getTag() != AudioPortExt::Tag::mix) continue; |
| const auto& mixPort = port.ext.get<AudioPortExt::Tag::mix>(); |
| if (port.flags.getTag() == AudioIoFlags::Tag::output && |
| ((port.flags.get<AudioIoFlags::Tag::output>() & primaryOutputFlag) != 0)) { |
| EXPECT_FALSE(primaryMixPort.has_value()) |
| << "At least two mix ports have PRIMARY flag set: " << primaryMixPort.value() |
| << " and " << port.id; |
| primaryMixPort = port.id; |
| EXPECT_EQ(1, mixPort.maxOpenStreamCount) |
| << "Primary mix port " << port.id << " can not have maxOpenStreamCount " |
| << mixPort.maxOpenStreamCount; |
| } |
| } |
| } |
| |
| TEST_P(AudioCoreModule, GetAudioPort) { |
| std::set<int32_t> portIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); |
| if (portIds.empty()) { |
| GTEST_SKIP() << "No ports in the module."; |
| } |
| for (const auto portId : portIds) { |
| AudioPort port; |
| ScopedAStatus status = module->getAudioPort(portId, &port); |
| EXPECT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| EXPECT_EQ(portId, port.id); |
| } |
| for (const auto portId : GetNonExistentIds(portIds)) { |
| AudioPort port; |
| ScopedAStatus status = module->getAudioPort(portId, &port); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for port ID " << portId; |
| } |
| } |
| |
| TEST_P(AudioCoreModule, SetUpModuleConfig) { |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| // Send the module config to logcat to facilitate failures investigation. |
| LOG(INFO) << "SetUpModuleConfig: " << moduleConfig->toString(); |
| } |
| |
| // Verify that HAL module reports for a connected device port at least one non-dynamic profile, |
| // that is, a profile with actual supported configuration. |
| // Note: This test relies on simulation of external device connections by the HAL module. |
| TEST_P(AudioCoreModule, GetAudioPortWithExternalDevices) { |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts(); |
| if (ports.empty()) { |
| GTEST_SKIP() << "No external devices in the module."; |
| } |
| for (const auto& port : ports) { |
| AudioPort portWithData = port; |
| portWithData.ext.get<AudioPortExt::Tag::device>().device.address = |
| GenerateUniqueDeviceAddress(); |
| WithDevicePortConnectedState portConnected(portWithData); |
| ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get())); |
| const int32_t connectedPortId = portConnected.getId(); |
| ASSERT_NE(portWithData.id, connectedPortId); |
| ASSERT_EQ(portWithData.ext.getTag(), portConnected.get().ext.getTag()); |
| EXPECT_EQ(portWithData.ext.get<AudioPortExt::Tag::device>().device, |
| portConnected.get().ext.get<AudioPortExt::Tag::device>().device); |
| // Verify that 'getAudioPort' and 'getAudioPorts' return the same connected port. |
| AudioPort connectedPort; |
| ScopedAStatus status = module->getAudioPort(connectedPortId, &connectedPort); |
| EXPECT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << " returned for getAudioPort port ID " << connectedPortId; |
| EXPECT_EQ(portConnected.get(), connectedPort); |
| const auto& portProfiles = connectedPort.profiles; |
| EXPECT_NE(0UL, portProfiles.size()) |
| << "Connected port has no profiles: " << connectedPort.toString(); |
| const auto dynamicProfileIt = |
| std::find_if(portProfiles.begin(), portProfiles.end(), [](const auto& profile) { |
| return profile.format.type == AudioFormatType::DEFAULT; |
| }); |
| EXPECT_EQ(portProfiles.end(), dynamicProfileIt) << "Connected port contains dynamic " |
| << "profiles: " << connectedPort.toString(); |
| |
| std::vector<AudioPort> allPorts; |
| { |
| ScopedAStatus status = module->getAudioPorts(&allPorts); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| const auto allPortsIt = findById(allPorts, connectedPortId); |
| EXPECT_NE(allPorts.end(), allPortsIt); |
| if (allPortsIt != allPorts.end()) { |
| EXPECT_EQ(portConnected.get(), *allPortsIt); |
| } |
| } |
| } |
| |
| TEST_P(AudioCoreModule, OpenStreamInvalidPortConfigId) { |
| std::set<int32_t> portConfigIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds)); |
| for (const auto portConfigId : GetNonExistentIds(portConfigIds)) { |
| { |
| aidl::android::hardware::audio::core::IModule::OpenInputStreamArguments args; |
| args.portConfigId = portConfigId; |
| args.bufferSizeFrames = kDefaultBufferSizeFrames; |
| aidl::android::hardware::audio::core::IModule::OpenInputStreamReturn ret; |
| ScopedAStatus status = module->openInputStream(args, &ret); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " openInputStream returned for port config ID " << portConfigId; |
| EXPECT_EQ(nullptr, ret.stream); |
| } |
| { |
| aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments args; |
| args.portConfigId = portConfigId; |
| args.bufferSizeFrames = kDefaultBufferSizeFrames; |
| aidl::android::hardware::audio::core::IModule::OpenOutputStreamReturn ret; |
| ScopedAStatus status = module->openOutputStream(args, &ret); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " openOutputStream returned for port config ID " << portConfigId; |
| EXPECT_EQ(nullptr, ret.stream); |
| } |
| } |
| } |
| |
| TEST_P(AudioCoreModule, PortConfigIdsAreUnique) { |
| std::set<int32_t> portConfigIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds)); |
| } |
| |
| TEST_P(AudioCoreModule, PortConfigPortIdsAreValid) { |
| std::set<int32_t> portIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); |
| std::vector<AudioPortConfig> portConfigs; |
| { |
| ScopedAStatus status = module->getAudioPortConfigs(&portConfigs); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| for (const auto& config : portConfigs) { |
| EXPECT_EQ(1UL, portIds.count(config.portId)) |
| << config.portId << " port id is unknown, config id " << config.id; |
| } |
| } |
| |
| TEST_P(AudioCoreModule, ResetAudioPortConfigInvalidId) { |
| std::set<int32_t> portConfigIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds)); |
| for (const auto portConfigId : GetNonExistentIds(portConfigIds)) { |
| ScopedAStatus status = module->resetAudioPortConfig(portConfigId); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for port config ID " << portConfigId; |
| } |
| } |
| |
| // Verify that for the audio port configs provided by the HAL after init, resetting |
| // the config does not delete it, but brings it back to the initial config. |
| TEST_P(AudioCoreModule, ResetAudioPortConfigToInitialValue) { |
| std::vector<AudioPortConfig> portConfigsBefore; |
| { |
| ScopedAStatus status = module->getAudioPortConfigs(&portConfigsBefore); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| // TODO: Change port configs according to port profiles. |
| for (const auto& c : portConfigsBefore) { |
| ScopedAStatus status = module->resetAudioPortConfig(c.id); |
| EXPECT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << " returned for port config ID " << c.id; |
| } |
| std::vector<AudioPortConfig> portConfigsAfter; |
| { |
| ScopedAStatus status = module->getAudioPortConfigs(&portConfigsAfter); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| for (const auto& c : portConfigsBefore) { |
| auto afterIt = findById<AudioPortConfig>(portConfigsAfter, c.id); |
| EXPECT_NE(portConfigsAfter.end(), afterIt) |
| << " port config ID " << c.id << " was removed by reset"; |
| if (afterIt != portConfigsAfter.end()) { |
| EXPECT_EQ(c, *afterIt); |
| } |
| } |
| } |
| |
| TEST_P(AudioCoreModule, SetAudioPortConfigSuggestedConfig) { |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| auto srcMixPort = moduleConfig->getSourceMixPortForAttachedDevice(); |
| if (!srcMixPort.has_value()) { |
| GTEST_SKIP() << "No mix port for attached output devices"; |
| } |
| AudioPortConfig portConfig; |
| AudioPortConfig suggestedConfig; |
| portConfig.portId = srcMixPort.value().id; |
| { |
| bool applied = true; |
| ScopedAStatus status = module->setAudioPortConfig(portConfig, &suggestedConfig, &applied); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << "; Config: " << portConfig.toString(); |
| EXPECT_FALSE(applied); |
| } |
| EXPECT_EQ(0, suggestedConfig.id); |
| EXPECT_TRUE(suggestedConfig.sampleRate.has_value()); |
| EXPECT_TRUE(suggestedConfig.channelMask.has_value()); |
| EXPECT_TRUE(suggestedConfig.format.has_value()); |
| EXPECT_TRUE(suggestedConfig.flags.has_value()); |
| WithAudioPortConfig applied(suggestedConfig); |
| ASSERT_NO_FATAL_FAILURE(applied.SetUp(module.get())); |
| const AudioPortConfig& appliedConfig = applied.get(); |
| EXPECT_NE(0, appliedConfig.id); |
| EXPECT_TRUE(appliedConfig.sampleRate.has_value()); |
| EXPECT_EQ(suggestedConfig.sampleRate.value(), appliedConfig.sampleRate.value()); |
| EXPECT_TRUE(appliedConfig.channelMask.has_value()); |
| EXPECT_EQ(suggestedConfig.channelMask.value(), appliedConfig.channelMask.value()); |
| EXPECT_TRUE(appliedConfig.format.has_value()); |
| EXPECT_EQ(suggestedConfig.format.value(), appliedConfig.format.value()); |
| EXPECT_TRUE(appliedConfig.flags.has_value()); |
| EXPECT_EQ(suggestedConfig.flags.value(), appliedConfig.flags.value()); |
| } |
| |
| TEST_P(AudioCoreModule, SetAllAttachedDevicePortConfigs) { |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| ASSERT_NO_FATAL_FAILURE(ApplyEveryConfig(moduleConfig->getPortConfigsForAttachedDevicePorts())); |
| } |
| |
| // Note: This test relies on simulation of external device connections by the HAL module. |
| TEST_P(AudioCoreModule, SetAllExternalDevicePortConfigs) { |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts(); |
| if (ports.empty()) { |
| GTEST_SKIP() << "No external devices in the module."; |
| } |
| for (const auto& port : ports) { |
| WithDevicePortConnectedState portConnected(port, GenerateUniqueDeviceAddress()); |
| ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get())); |
| ASSERT_NO_FATAL_FAILURE( |
| ApplyEveryConfig(moduleConfig->getPortConfigsForDevicePort(portConnected.get()))); |
| } |
| } |
| |
| TEST_P(AudioCoreModule, SetAllStaticAudioPortConfigs) { |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| ASSERT_NO_FATAL_FAILURE(ApplyEveryConfig(moduleConfig->getPortConfigsForMixPorts())); |
| } |
| |
| TEST_P(AudioCoreModule, SetAudioPortConfigInvalidPortId) { |
| std::set<int32_t> portIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); |
| for (const auto portId : GetNonExistentIds(portIds)) { |
| AudioPortConfig portConfig, suggestedConfig; |
| bool applied; |
| portConfig.portId = portId; |
| ScopedAStatus status = module->setAudioPortConfig(portConfig, &suggestedConfig, &applied); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for port ID " << portId; |
| EXPECT_FALSE(suggestedConfig.format.has_value()); |
| EXPECT_FALSE(suggestedConfig.channelMask.has_value()); |
| EXPECT_FALSE(suggestedConfig.sampleRate.has_value()); |
| } |
| } |
| |
| TEST_P(AudioCoreModule, SetAudioPortConfigInvalidPortConfigId) { |
| std::set<int32_t> portConfigIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds)); |
| for (const auto portConfigId : GetNonExistentIds(portConfigIds)) { |
| AudioPortConfig portConfig, suggestedConfig; |
| bool applied; |
| portConfig.id = portConfigId; |
| ScopedAStatus status = module->setAudioPortConfig(portConfig, &suggestedConfig, &applied); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for port config ID " << portConfigId; |
| EXPECT_FALSE(suggestedConfig.format.has_value()); |
| EXPECT_FALSE(suggestedConfig.channelMask.has_value()); |
| EXPECT_FALSE(suggestedConfig.sampleRate.has_value()); |
| } |
| } |
| |
| TEST_P(AudioCoreModule, TryConnectMissingDevice) { |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts(); |
| if (ports.empty()) { |
| GTEST_SKIP() << "No external devices in the module."; |
| } |
| AudioPort ignored; |
| WithDebugFlags doNotSimulateConnections = WithDebugFlags::createNested(debug); |
| doNotSimulateConnections.flags().simulateDeviceConnections = false; |
| ASSERT_NO_FATAL_FAILURE(doNotSimulateConnections.SetUp(module.get())); |
| for (const auto& port : ports) { |
| AudioPort portWithData = port; |
| portWithData.ext.get<AudioPortExt::Tag::device>().device.address = |
| GenerateUniqueDeviceAddress(); |
| ScopedAStatus status = module->connectExternalDevice(portWithData, &ignored); |
| EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) |
| << status << " returned for static port " << portWithData.toString(); |
| } |
| } |
| |
| TEST_P(AudioCoreModule, TryChangingConnectionSimulationMidway) { |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts(); |
| if (ports.empty()) { |
| GTEST_SKIP() << "No external devices in the module."; |
| } |
| WithDevicePortConnectedState portConnected(*ports.begin(), GenerateUniqueDeviceAddress()); |
| ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get())); |
| ModuleDebug midwayDebugChange = debug.flags(); |
| midwayDebugChange.simulateDeviceConnections = false; |
| ScopedAStatus status = module->setModuleDebug(midwayDebugChange); |
| EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) |
| << status << " returned when trying to disable connections simulation " |
| << "while having a connected device"; |
| } |
| |
| TEST_P(AudioCoreModule, ConnectDisconnectExternalDeviceInvalidPorts) { |
| AudioPort ignored; |
| std::set<int32_t> portIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortIds(&portIds)); |
| for (const auto portId : GetNonExistentIds(portIds)) { |
| AudioPort invalidPort; |
| invalidPort.id = portId; |
| ScopedAStatus status = module->connectExternalDevice(invalidPort, &ignored); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for port ID " << portId << " when setting CONNECTED state"; |
| status = module->disconnectExternalDevice(portId); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for port ID " << portId |
| << " when setting DISCONNECTED state"; |
| } |
| |
| std::vector<AudioPort> ports; |
| { |
| ScopedAStatus status = module->getAudioPorts(&ports); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| for (const auto& port : ports) { |
| if (port.ext.getTag() != AudioPortExt::Tag::device) { |
| ScopedAStatus status = module->connectExternalDevice(port, &ignored); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for non-device port ID " << port.id |
| << " when setting CONNECTED state"; |
| status = module->disconnectExternalDevice(port.id); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for non-device port ID " << port.id |
| << " when setting DISCONNECTED state"; |
| } else { |
| const auto& devicePort = port.ext.get<AudioPortExt::Tag::device>(); |
| if (devicePort.device.type.connection.empty()) { |
| ScopedAStatus status = module->connectExternalDevice(port, &ignored); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for permanently attached device port ID " << port.id |
| << " when setting CONNECTED state"; |
| status = module->disconnectExternalDevice(port.id); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for permanently attached device port ID " << port.id |
| << " when setting DISCONNECTED state"; |
| } |
| } |
| } |
| } |
| |
| // Note: This test relies on simulation of external device connections by the HAL module. |
| TEST_P(AudioCoreModule, ConnectDisconnectExternalDeviceTwice) { |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| AudioPort ignored; |
| std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts(); |
| if (ports.empty()) { |
| GTEST_SKIP() << "No external devices in the module."; |
| } |
| for (const auto& port : ports) { |
| ScopedAStatus status = module->disconnectExternalDevice(port.id); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned when disconnecting already disconnected device port ID " |
| << port.id; |
| AudioPort portWithData = port; |
| portWithData.ext.get<AudioPortExt::Tag::device>().device.address = |
| GenerateUniqueDeviceAddress(); |
| WithDevicePortConnectedState portConnected(portWithData); |
| ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get())); |
| status = module->connectExternalDevice(portConnected.get(), &ignored); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned when trying to connect a connected device port " |
| << portConnected.get().toString(); |
| status = module->connectExternalDevice(portWithData, &ignored); |
| EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) |
| << status << " returned when connecting again the external device " |
| << portWithData.ext.get<AudioPortExt::Tag::device>().device.toString(); |
| if (status.getExceptionCode() == EX_NONE) { |
| ADD_FAILURE() << "Returned connected port " << ignored.toString() << " for template " |
| << portWithData.toString(); |
| } |
| } |
| } |
| |
| // Note: This test relies on simulation of external device connections by the HAL module. |
| TEST_P(AudioCoreModule, DisconnectExternalDeviceNonResetPortConfig) { |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts(); |
| if (ports.empty()) { |
| GTEST_SKIP() << "No external devices in the module."; |
| } |
| for (const auto& port : ports) { |
| WithDevicePortConnectedState portConnected(port, GenerateUniqueDeviceAddress()); |
| ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get())); |
| const auto portConfig = moduleConfig->getSingleConfigForDevicePort(portConnected.get()); |
| { |
| WithAudioPortConfig config(portConfig); |
| // Note: if SetUp fails, check the status of 'GetAudioPortWithExternalDevices' test. |
| // Our test assumes that 'getAudioPort' returns at least one profile, and it |
| // is not a dynamic profile. |
| ASSERT_NO_FATAL_FAILURE(config.SetUp(module.get())); |
| ScopedAStatus status = module->disconnectExternalDevice(portConnected.getId()); |
| EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) |
| << status << " returned when trying to disconnect device port ID " << port.id |
| << " with active configuration " << config.getId(); |
| } |
| } |
| } |
| |
| TEST_P(AudioCoreModule, ExternalDevicePortRoutes) { |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| std::vector<AudioPort> ports = moduleConfig->getExternalDevicePorts(); |
| if (ports.empty()) { |
| GTEST_SKIP() << "No external devices in the module."; |
| } |
| for (const auto& port : ports) { |
| std::vector<AudioRoute> routesBefore; |
| { |
| ScopedAStatus status = module->getAudioRoutes(&routesBefore); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| |
| int32_t connectedPortId; |
| { |
| WithDevicePortConnectedState portConnected(port, GenerateUniqueDeviceAddress()); |
| ASSERT_NO_FATAL_FAILURE(portConnected.SetUp(module.get())); |
| connectedPortId = portConnected.getId(); |
| std::vector<AudioRoute> connectedPortRoutes; |
| { |
| ScopedAStatus status = |
| module->getAudioRoutesForAudioPort(connectedPortId, &connectedPortRoutes); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) |
| << status << " returned when retrieving routes for connected port id " |
| << connectedPortId; |
| } |
| // There must be routes for the port to be useful. |
| if (connectedPortRoutes.empty()) { |
| std::vector<AudioRoute> allRoutes; |
| ScopedAStatus status = module->getAudioRoutes(&allRoutes); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| ADD_FAILURE() << " no routes returned for the connected port " |
| << portConnected.get().toString() |
| << "; all routes: " << android::internal::ToString(allRoutes); |
| } |
| } |
| std::vector<AudioRoute> ignored; |
| ScopedAStatus status = module->getAudioRoutesForAudioPort(connectedPortId, &ignored); |
| ASSERT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned when retrieving routes for released connected port id " |
| << connectedPortId; |
| |
| std::vector<AudioRoute> routesAfter; |
| { |
| ScopedAStatus status = module->getAudioRoutes(&routesAfter); |
| ASSERT_EQ(EX_NONE, status.getExceptionCode()) << status; |
| } |
| ASSERT_EQ(routesBefore.size(), routesAfter.size()) |
| << "Sizes of audio route arrays do not match after creating and " |
| << "releasing a connected port"; |
| std::sort(routesBefore.begin(), routesBefore.end()); |
| std::sort(routesAfter.begin(), routesAfter.end()); |
| EXPECT_EQ(routesBefore, routesAfter); |
| } |
| } |
| |
| template <typename Stream> |
| class AudioStream : public AudioCoreModule { |
| public: |
| static std::string direction(bool capitalize); |
| |
| void SetUp() override { |
| ASSERT_NO_FATAL_FAILURE(AudioCoreModule::SetUp()); |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| } |
| |
| void CloseTwice() { |
| const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input); |
| if (!portConfig.has_value()) { |
| GTEST_SKIP() << "No mix port for attached devices"; |
| } |
| std::shared_ptr<Stream> heldStream; |
| { |
| WithStream<Stream> stream(portConfig.value()); |
| ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames)); |
| heldStream = stream.getSharedPointer(); |
| } |
| ScopedAStatus status = heldStream->close(); |
| EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) |
| << status << " when closing the stream twice"; |
| } |
| |
| void OpenAllConfigs() { |
| const auto allPortConfigs = |
| moduleConfig->getPortConfigsForMixPorts(IOTraits<Stream>::is_input); |
| for (const auto& portConfig : allPortConfigs) { |
| WithStream<Stream> stream(portConfig); |
| ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames)); |
| } |
| } |
| |
| void OpenInvalidBufferSize() { |
| const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input); |
| if (!portConfig.has_value()) { |
| GTEST_SKIP() << "No mix port for attached devices"; |
| } |
| WithStream<Stream> stream(portConfig.value()); |
| ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfig(module.get())); |
| // The buffer size of 1 frame should be impractically small, and thus |
| // less than any minimum buffer size suggested by any HAL. |
| for (long bufferSize : std::array<long, 4>{-1, 0, 1, std::numeric_limits<long>::max()}) { |
| ScopedAStatus status = stream.SetUpNoChecks(module.get(), bufferSize); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " open" << direction(true) << "Stream returned for " << bufferSize |
| << " buffer size"; |
| EXPECT_EQ(nullptr, stream.get()); |
| } |
| } |
| |
| void OpenInvalidDirection() { |
| // Important! The direction of the port config must be reversed. |
| const auto portConfig = |
| moduleConfig->getSingleConfigForMixPort(!IOTraits<Stream>::is_input); |
| if (!portConfig.has_value()) { |
| GTEST_SKIP() << "No mix port for attached devices"; |
| } |
| WithStream<Stream> stream(portConfig.value()); |
| ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfig(module.get())); |
| ScopedAStatus status = stream.SetUpNoChecks(module.get(), kDefaultBufferSizeFrames); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " open" << direction(true) << "Stream returned for port config ID " |
| << stream.getPortId(); |
| EXPECT_EQ(nullptr, stream.get()); |
| } |
| |
| void OpenOverMaxCount() { |
| constexpr bool isInput = IOTraits<Stream>::is_input; |
| auto ports = moduleConfig->getMixPorts(isInput); |
| bool hasSingleRun = false; |
| for (const auto& port : ports) { |
| const size_t maxStreamCount = port.ext.get<AudioPortExt::Tag::mix>().maxOpenStreamCount; |
| if (maxStreamCount == 0 || |
| moduleConfig->getAttachedDevicesPortsForMixPort(isInput, port).empty()) { |
| // No restrictions or no permanently attached devices. |
| continue; |
| } |
| auto portConfigs = moduleConfig->getPortConfigsForMixPorts(isInput, port); |
| if (portConfigs.size() < maxStreamCount + 1) { |
| // Not able to open a sufficient number of streams for this port. |
| continue; |
| } |
| hasSingleRun = true; |
| std::optional<WithStream<Stream>> streamWraps[maxStreamCount + 1]; |
| for (size_t i = 0; i <= maxStreamCount; ++i) { |
| streamWraps[i].emplace(portConfigs[i]); |
| WithStream<Stream>& stream = streamWraps[i].value(); |
| if (i < maxStreamCount) { |
| ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames)); |
| } else { |
| ASSERT_NO_FATAL_FAILURE(stream.SetUpPortConfig(module.get())); |
| ScopedAStatus status = |
| stream.SetUpNoChecks(module.get(), kDefaultBufferSizeFrames); |
| EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) |
| << status << " open" << direction(true) |
| << "Stream returned for port config ID " << stream.getPortId() |
| << ", maxOpenStreamCount is " << maxStreamCount; |
| } |
| } |
| } |
| if (!hasSingleRun) { |
| GTEST_SKIP() << "Not enough " << direction(false) |
| << " ports to test max open stream count"; |
| } |
| } |
| |
| void OpenTwiceSamePortConfig() { |
| const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input); |
| if (!portConfig.has_value()) { |
| GTEST_SKIP() << "No mix port for attached devices"; |
| } |
| EXPECT_NO_FATAL_FAILURE(OpenTwiceSamePortConfigImpl(portConfig.value())); |
| } |
| |
| void ReadOrWrite(bool useImpl2, bool testObservablePosition) { |
| const auto allPortConfigs = |
| moduleConfig->getPortConfigsForMixPorts(IOTraits<Stream>::is_input); |
| if (allPortConfigs.empty()) { |
| GTEST_SKIP() << "No mix ports have attached devices"; |
| } |
| for (const auto& portConfig : allPortConfigs) { |
| EXPECT_NO_FATAL_FAILURE(ReadOrWriteImpl(portConfig, useImpl2, testObservablePosition)) |
| << portConfig.toString(); |
| } |
| } |
| |
| void ResetPortConfigWithOpenStream() { |
| const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input); |
| if (!portConfig.has_value()) { |
| GTEST_SKIP() << "No mix port for attached devices"; |
| } |
| WithStream<Stream> stream(portConfig.value()); |
| ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames)); |
| ScopedAStatus status = module->resetAudioPortConfig(stream.getPortId()); |
| EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) |
| << status << " returned for port config ID " << stream.getPortId(); |
| } |
| |
| void SendInvalidCommand() { |
| const auto portConfig = moduleConfig->getSingleConfigForMixPort(IOTraits<Stream>::is_input); |
| if (!portConfig.has_value()) { |
| GTEST_SKIP() << "No mix port for attached devices"; |
| } |
| EXPECT_NO_FATAL_FAILURE(SendInvalidCommandImpl(portConfig.value())); |
| } |
| |
| void OpenTwiceSamePortConfigImpl(const AudioPortConfig& portConfig) { |
| WithStream<Stream> stream1(portConfig); |
| ASSERT_NO_FATAL_FAILURE(stream1.SetUp(module.get(), kDefaultBufferSizeFrames)); |
| WithStream<Stream> stream2; |
| ScopedAStatus status = stream2.SetUpNoChecks(module.get(), stream1.getPortConfig(), |
| kDefaultBufferSizeFrames); |
| EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) |
| << status << " when opening " << direction(false) |
| << " stream twice for the same port config ID " << stream1.getPortId(); |
| } |
| |
| template <class Worker> |
| void WaitForObservablePositionAdvance(Worker& worker) { |
| static constexpr int kWriteDurationUs = 50 * 1000; |
| static constexpr std::chrono::milliseconds kPositionChangeTimeout{10000}; |
| int64_t framesInitial; |
| framesInitial = worker.getLastObservablePosition().frames; |
| ASSERT_FALSE(worker.hasError()); |
| bool timedOut = false; |
| int64_t frames = framesInitial; |
| for (android::base::Timer elapsed; |
| frames <= framesInitial && !worker.hasError() && |
| !(timedOut = (elapsed.duration() >= kPositionChangeTimeout));) { |
| usleep(kWriteDurationUs); |
| frames = worker.getLastObservablePosition().frames; |
| } |
| EXPECT_FALSE(timedOut); |
| EXPECT_FALSE(worker.hasError()) << worker.getError(); |
| EXPECT_GT(frames, framesInitial); |
| } |
| |
| void ReadOrWriteImpl(const AudioPortConfig& portConfig, bool useImpl2, |
| bool testObservablePosition) { |
| if (!useImpl2) { |
| ASSERT_NO_FATAL_FAILURE(ReadOrWriteImpl1(portConfig, testObservablePosition)); |
| } else { |
| ASSERT_NO_FATAL_FAILURE(ReadOrWriteImpl2(portConfig, testObservablePosition)); |
| } |
| } |
| |
| // Set up a patch first, then open a stream. |
| void ReadOrWriteImpl1(const AudioPortConfig& portConfig, bool testObservablePosition) { |
| auto devicePorts = moduleConfig->getAttachedDevicesPortsForMixPort( |
| IOTraits<Stream>::is_input, portConfig); |
| ASSERT_FALSE(devicePorts.empty()); |
| auto devicePortConfig = moduleConfig->getSingleConfigForDevicePort(devicePorts[0]); |
| WithAudioPatch patch(IOTraits<Stream>::is_input, portConfig, devicePortConfig); |
| ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get())); |
| |
| WithStream<Stream> stream(patch.getPortConfig(IOTraits<Stream>::is_input)); |
| ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames)); |
| typename IOTraits<Stream>::Worker worker(*stream.getContext()); |
| |
| ASSERT_TRUE(worker.start()); |
| ASSERT_TRUE(worker.waitForAtLeastOneCycle()); |
| if (testObservablePosition) { |
| ASSERT_NO_FATAL_FAILURE(WaitForObservablePositionAdvance(worker)); |
| } |
| } |
| |
| // Open a stream, then set up a patch for it. |
| void ReadOrWriteImpl2(const AudioPortConfig& portConfig, bool testObservablePosition) { |
| WithStream<Stream> stream(portConfig); |
| ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames)); |
| typename IOTraits<Stream>::Worker worker(*stream.getContext()); |
| |
| auto devicePorts = moduleConfig->getAttachedDevicesPortsForMixPort( |
| IOTraits<Stream>::is_input, portConfig); |
| ASSERT_FALSE(devicePorts.empty()); |
| auto devicePortConfig = moduleConfig->getSingleConfigForDevicePort(devicePorts[0]); |
| WithAudioPatch patch(IOTraits<Stream>::is_input, stream.getPortConfig(), devicePortConfig); |
| ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get())); |
| |
| ASSERT_TRUE(worker.start()); |
| ASSERT_TRUE(worker.waitForAtLeastOneCycle()); |
| if (testObservablePosition) { |
| ASSERT_NO_FATAL_FAILURE(WaitForObservablePositionAdvance(worker)); |
| } |
| } |
| |
| void SendInvalidCommandImpl(const AudioPortConfig& portConfig) { |
| std::vector<StreamDescriptor::Command> commands(6); |
| commands[0].code = -1; |
| commands[1].code = StreamDescriptor::COMMAND_BURST - 1; |
| commands[2].code = std::numeric_limits<int32_t>::min(); |
| commands[3].code = std::numeric_limits<int32_t>::max(); |
| commands[4].code = StreamDescriptor::COMMAND_BURST; |
| commands[4].fmqByteCount = -1; |
| commands[5].code = StreamDescriptor::COMMAND_BURST; |
| commands[5].fmqByteCount = std::numeric_limits<int32_t>::min(); |
| WithStream<Stream> stream(portConfig); |
| ASSERT_NO_FATAL_FAILURE(stream.SetUp(module.get(), kDefaultBufferSizeFrames)); |
| StreamWorker<StreamInvalidCommandLogic> writer(*stream.getContext(), commands); |
| ASSERT_TRUE(writer.start()); |
| writer.waitForAtLeastOneCycle(); |
| auto unexpectedStatuses = writer.getUnexpectedStatuses(); |
| EXPECT_EQ(0UL, unexpectedStatuses.size()) |
| << "Pairs of (command, actual status): " |
| << android::internal::ToString(unexpectedStatuses); |
| } |
| }; |
| using AudioStreamIn = AudioStream<IStreamIn>; |
| using AudioStreamOut = AudioStream<IStreamOut>; |
| |
| template <> |
| std::string AudioStreamIn::direction(bool capitalize) { |
| return capitalize ? "Input" : "input"; |
| } |
| template <> |
| std::string AudioStreamOut::direction(bool capitalize) { |
| return capitalize ? "Output" : "output"; |
| } |
| |
| #define TEST_IO_STREAM(method_name) \ |
| TEST_P(AudioStreamIn, method_name) { ASSERT_NO_FATAL_FAILURE(method_name()); } \ |
| TEST_P(AudioStreamOut, method_name) { ASSERT_NO_FATAL_FAILURE(method_name()); } |
| #define TEST_IO_STREAM_2(method_name, arg1, arg2) \ |
| TEST_P(AudioStreamIn, method_name##_##arg1##_##arg2) { \ |
| ASSERT_NO_FATAL_FAILURE(method_name(arg1, arg2)); \ |
| } \ |
| TEST_P(AudioStreamOut, method_name##_##arg1##_##arg2) { \ |
| ASSERT_NO_FATAL_FAILURE(method_name(arg1, arg2)); \ |
| } |
| |
| TEST_IO_STREAM(CloseTwice); |
| TEST_IO_STREAM(OpenAllConfigs); |
| TEST_IO_STREAM(OpenInvalidBufferSize); |
| TEST_IO_STREAM(OpenInvalidDirection); |
| TEST_IO_STREAM(OpenOverMaxCount); |
| TEST_IO_STREAM(OpenTwiceSamePortConfig); |
| TEST_IO_STREAM_2(ReadOrWrite, false, false); |
| TEST_IO_STREAM_2(ReadOrWrite, true, false); |
| TEST_IO_STREAM_2(ReadOrWrite, false, true); |
| TEST_IO_STREAM_2(ReadOrWrite, true, true); |
| TEST_IO_STREAM(ResetPortConfigWithOpenStream); |
| TEST_IO_STREAM(SendInvalidCommand); |
| |
| TEST_P(AudioStreamOut, OpenTwicePrimary) { |
| const auto mixPorts = moduleConfig->getMixPorts(false); |
| auto primaryPortIt = std::find_if(mixPorts.begin(), mixPorts.end(), [](const AudioPort& port) { |
| constexpr int primaryOutputFlag = 1 << static_cast<int>(AudioOutputFlags::PRIMARY); |
| return port.flags.getTag() == AudioIoFlags::Tag::output && |
| (port.flags.get<AudioIoFlags::Tag::output>() & primaryOutputFlag) != 0; |
| }); |
| if (primaryPortIt == mixPorts.end()) { |
| GTEST_SKIP() << "No primary mix port"; |
| } |
| if (moduleConfig->getAttachedSinkDevicesPortsForMixPort(*primaryPortIt).empty()) { |
| GTEST_SKIP() << "Primary mix port can not be routed to any of attached devices"; |
| } |
| const auto portConfig = moduleConfig->getSingleConfigForMixPort(false, *primaryPortIt); |
| ASSERT_TRUE(portConfig.has_value()) << "No profiles specified for the primary mix port"; |
| EXPECT_NO_FATAL_FAILURE(OpenTwiceSamePortConfigImpl(portConfig.value())); |
| } |
| |
| TEST_P(AudioStreamOut, RequireOffloadInfo) { |
| const auto mixPorts = moduleConfig->getMixPorts(false); |
| auto offloadPortIt = std::find_if(mixPorts.begin(), mixPorts.end(), [&](const AudioPort& port) { |
| constexpr int compressOffloadFlag = 1 |
| << static_cast<int>(AudioOutputFlags::COMPRESS_OFFLOAD); |
| return port.flags.getTag() == AudioIoFlags::Tag::output && |
| (port.flags.get<AudioIoFlags::Tag::output>() & compressOffloadFlag) != 0 && |
| !moduleConfig->getAttachedSinkDevicesPortsForMixPort(port).empty(); |
| }); |
| if (offloadPortIt == mixPorts.end()) { |
| GTEST_SKIP() |
| << "No mix port for compressed offload that could be routed to attached devices"; |
| } |
| const auto portConfig = moduleConfig->getSingleConfigForMixPort(false, *offloadPortIt); |
| ASSERT_TRUE(portConfig.has_value()) |
| << "No profiles specified for the compressed offload mix port"; |
| StreamDescriptor descriptor; |
| std::shared_ptr<IStreamOut> ignored; |
| aidl::android::hardware::audio::core::IModule::OpenOutputStreamArguments args; |
| args.portConfigId = portConfig.value().id; |
| args.sourceMetadata = GenerateSourceMetadata(portConfig.value()); |
| args.bufferSizeFrames = kDefaultBufferSizeFrames; |
| aidl::android::hardware::audio::core::IModule::OpenOutputStreamReturn ret; |
| ScopedAStatus status = module->openOutputStream(args, &ret); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status |
| << " returned when no offload info is provided for a compressed offload mix port"; |
| } |
| |
| // Tests specific to audio patches. The fixure class is named 'AudioModulePatch' |
| // to avoid clashing with 'AudioPatch' class. |
| class AudioModulePatch : public AudioCoreModule { |
| public: |
| static std::string direction(bool isInput, bool capitalize) { |
| return isInput ? (capitalize ? "Input" : "input") : (capitalize ? "Output" : "output"); |
| } |
| |
| void SetUp() override { |
| ASSERT_NO_FATAL_FAILURE(AudioCoreModule::SetUp()); |
| ASSERT_NO_FATAL_FAILURE(SetUpModuleConfig()); |
| } |
| |
| void SetInvalidPatchHelper(int32_t expectedException, const std::vector<int32_t>& sources, |
| const std::vector<int32_t>& sinks) { |
| AudioPatch patch; |
| patch.sourcePortConfigIds = sources; |
| patch.sinkPortConfigIds = sinks; |
| ScopedAStatus status = module->setAudioPatch(patch, &patch); |
| ASSERT_EQ(expectedException, status.getExceptionCode()) |
| << status << ": patch source ids: " << android::internal::ToString(sources) |
| << "; sink ids: " << android::internal::ToString(sinks); |
| } |
| |
| void ResetPortConfigUsedByPatch(bool isInput) { |
| auto srcSinkGroups = moduleConfig->getRoutableSrcSinkGroups(isInput); |
| if (srcSinkGroups.empty()) { |
| GTEST_SKIP() << "No routes to any attached " << direction(isInput, false) << " devices"; |
| } |
| auto srcSinkGroup = *srcSinkGroups.begin(); |
| auto srcSink = *srcSinkGroup.second.begin(); |
| WithAudioPatch patch(srcSink.first, srcSink.second); |
| ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get())); |
| std::vector<int32_t> sourceAndSinkPortConfigIds(patch.get().sourcePortConfigIds); |
| sourceAndSinkPortConfigIds.insert(sourceAndSinkPortConfigIds.end(), |
| patch.get().sinkPortConfigIds.begin(), |
| patch.get().sinkPortConfigIds.end()); |
| for (const auto portConfigId : sourceAndSinkPortConfigIds) { |
| ScopedAStatus status = module->resetAudioPortConfig(portConfigId); |
| EXPECT_EQ(EX_ILLEGAL_STATE, status.getExceptionCode()) |
| << status << " returned for port config ID " << portConfigId; |
| } |
| } |
| |
| void SetInvalidPatch(bool isInput) { |
| auto srcSinkPair = moduleConfig->getRoutableSrcSinkPair(isInput); |
| if (!srcSinkPair.has_value()) { |
| GTEST_SKIP() << "No routes to any attached " << direction(isInput, false) << " devices"; |
| } |
| WithAudioPortConfig srcPortConfig(srcSinkPair.value().first); |
| ASSERT_NO_FATAL_FAILURE(srcPortConfig.SetUp(module.get())); |
| WithAudioPortConfig sinkPortConfig(srcSinkPair.value().second); |
| ASSERT_NO_FATAL_FAILURE(sinkPortConfig.SetUp(module.get())); |
| { // Check that the pair can actually be used for setting up a patch. |
| WithAudioPatch patch(srcPortConfig.get(), sinkPortConfig.get()); |
| ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get())); |
| } |
| EXPECT_NO_FATAL_FAILURE( |
| SetInvalidPatchHelper(EX_ILLEGAL_ARGUMENT, {}, {sinkPortConfig.getId()})); |
| EXPECT_NO_FATAL_FAILURE(SetInvalidPatchHelper( |
| EX_ILLEGAL_ARGUMENT, {srcPortConfig.getId(), srcPortConfig.getId()}, |
| {sinkPortConfig.getId()})); |
| EXPECT_NO_FATAL_FAILURE( |
| SetInvalidPatchHelper(EX_ILLEGAL_ARGUMENT, {srcPortConfig.getId()}, {})); |
| EXPECT_NO_FATAL_FAILURE( |
| SetInvalidPatchHelper(EX_ILLEGAL_ARGUMENT, {srcPortConfig.getId()}, |
| {sinkPortConfig.getId(), sinkPortConfig.getId()})); |
| |
| std::set<int32_t> portConfigIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPortConfigIds(&portConfigIds)); |
| for (const auto portConfigId : GetNonExistentIds(portConfigIds)) { |
| EXPECT_NO_FATAL_FAILURE(SetInvalidPatchHelper(EX_ILLEGAL_ARGUMENT, {portConfigId}, |
| {sinkPortConfig.getId()})); |
| EXPECT_NO_FATAL_FAILURE(SetInvalidPatchHelper(EX_ILLEGAL_ARGUMENT, |
| {srcPortConfig.getId()}, {portConfigId})); |
| } |
| } |
| |
| void SetNonRoutablePatch(bool isInput) { |
| auto srcSinkPair = moduleConfig->getNonRoutableSrcSinkPair(isInput); |
| if (!srcSinkPair.has_value()) { |
| GTEST_SKIP() << "All possible source/sink pairs are routable"; |
| } |
| WithAudioPatch patch(srcSinkPair.value().first, srcSinkPair.value().second); |
| ASSERT_NO_FATAL_FAILURE(patch.SetUpPortConfigs(module.get())); |
| ScopedAStatus status = patch.SetUpNoChecks(module.get()); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << ": when setting up a patch from " |
| << srcSinkPair.value().first.toString() << " to " |
| << srcSinkPair.value().second.toString() << " that does not have a route"; |
| } |
| |
| void SetPatch(bool isInput) { |
| auto srcSinkGroups = moduleConfig->getRoutableSrcSinkGroups(isInput); |
| if (srcSinkGroups.empty()) { |
| GTEST_SKIP() << "No routes to any attached " << direction(isInput, false) << " devices"; |
| } |
| for (const auto& srcSinkGroup : srcSinkGroups) { |
| const auto& route = srcSinkGroup.first; |
| std::vector<std::unique_ptr<WithAudioPatch>> patches; |
| for (const auto& srcSink : srcSinkGroup.second) { |
| if (!route.isExclusive) { |
| patches.push_back( |
| std::make_unique<WithAudioPatch>(srcSink.first, srcSink.second)); |
| EXPECT_NO_FATAL_FAILURE(patches[patches.size() - 1]->SetUp(module.get())); |
| } else { |
| WithAudioPatch patch(srcSink.first, srcSink.second); |
| EXPECT_NO_FATAL_FAILURE(patch.SetUp(module.get())); |
| } |
| } |
| } |
| } |
| |
| void UpdatePatch(bool isInput) { |
| auto srcSinkGroups = moduleConfig->getRoutableSrcSinkGroups(isInput); |
| if (srcSinkGroups.empty()) { |
| GTEST_SKIP() << "No routes to any attached " << direction(isInput, false) << " devices"; |
| } |
| for (const auto& srcSinkGroup : srcSinkGroups) { |
| for (const auto& srcSink : srcSinkGroup.second) { |
| WithAudioPatch patch(srcSink.first, srcSink.second); |
| ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get())); |
| AudioPatch ignored; |
| EXPECT_NO_FATAL_FAILURE(module->setAudioPatch(patch.get(), &ignored)); |
| } |
| } |
| } |
| |
| void UpdateInvalidPatchId(bool isInput) { |
| auto srcSinkGroups = moduleConfig->getRoutableSrcSinkGroups(isInput); |
| if (srcSinkGroups.empty()) { |
| GTEST_SKIP() << "No routes to any attached " << direction(isInput, false) << " devices"; |
| } |
| // First, set up a patch to ensure that its settings are accepted. |
| auto srcSinkGroup = *srcSinkGroups.begin(); |
| auto srcSink = *srcSinkGroup.second.begin(); |
| WithAudioPatch patch(srcSink.first, srcSink.second); |
| ASSERT_NO_FATAL_FAILURE(patch.SetUp(module.get())); |
| // Then use the same patch setting, except for having an invalid ID. |
| std::set<int32_t> patchIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPatchIds(&patchIds)); |
| for (const auto patchId : GetNonExistentIds(patchIds)) { |
| AudioPatch patchWithNonExistendId = patch.get(); |
| patchWithNonExistendId.id = patchId; |
| ScopedAStatus status = |
| module->setAudioPatch(patchWithNonExistendId, &patchWithNonExistendId); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for patch ID " << patchId; |
| } |
| } |
| }; |
| |
| // Not all tests require both directions, so parametrization would require |
| // more abstractions. |
| #define TEST_PATCH_BOTH_DIRECTIONS(method_name) \ |
| TEST_P(AudioModulePatch, method_name##Input) { ASSERT_NO_FATAL_FAILURE(method_name(true)); } \ |
| TEST_P(AudioModulePatch, method_name##Output) { ASSERT_NO_FATAL_FAILURE(method_name(false)); } |
| |
| TEST_PATCH_BOTH_DIRECTIONS(ResetPortConfigUsedByPatch); |
| TEST_PATCH_BOTH_DIRECTIONS(SetInvalidPatch); |
| TEST_PATCH_BOTH_DIRECTIONS(SetNonRoutablePatch); |
| TEST_PATCH_BOTH_DIRECTIONS(SetPatch); |
| TEST_PATCH_BOTH_DIRECTIONS(UpdateInvalidPatchId); |
| TEST_PATCH_BOTH_DIRECTIONS(UpdatePatch); |
| |
| TEST_P(AudioModulePatch, ResetInvalidPatchId) { |
| std::set<int32_t> patchIds; |
| ASSERT_NO_FATAL_FAILURE(GetAllPatchIds(&patchIds)); |
| for (const auto patchId : GetNonExistentIds(patchIds)) { |
| ScopedAStatus status = module->resetAudioPatch(patchId); |
| EXPECT_EQ(EX_ILLEGAL_ARGUMENT, status.getExceptionCode()) |
| << status << " returned for patch ID " << patchId; |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(AudioCoreModuleTest, AudioCoreModule, |
| testing::ValuesIn(android::getAidlHalInstanceNames(IModule::descriptor)), |
| android::PrintInstanceNameToString); |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioCoreModule); |
| INSTANTIATE_TEST_SUITE_P(AudioStreamInTest, AudioStreamIn, |
| testing::ValuesIn(android::getAidlHalInstanceNames(IModule::descriptor)), |
| android::PrintInstanceNameToString); |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioStreamIn); |
| INSTANTIATE_TEST_SUITE_P(AudioStreamOutTest, AudioStreamOut, |
| testing::ValuesIn(android::getAidlHalInstanceNames(IModule::descriptor)), |
| android::PrintInstanceNameToString); |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioStreamOut); |
| INSTANTIATE_TEST_SUITE_P(AudioPatchTest, AudioModulePatch, |
| testing::ValuesIn(android::getAidlHalInstanceNames(IModule::descriptor)), |
| android::PrintInstanceNameToString); |
| GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(AudioModulePatch); |
| |
| class TestExecutionTracer : public ::testing::EmptyTestEventListener { |
| public: |
| void OnTestStart(const ::testing::TestInfo& test_info) override { |
| TraceTestState("Started", test_info); |
| } |
| |
| void OnTestEnd(const ::testing::TestInfo& test_info) override { |
| TraceTestState("Completed", test_info); |
| } |
| |
| private: |
| static void TraceTestState(const std::string& state, const ::testing::TestInfo& test_info) { |
| LOG(INFO) << state << " " << test_info.test_suite_name() << "::" << test_info.name(); |
| } |
| }; |
| |
| int main(int argc, char** argv) { |
| ::testing::InitGoogleTest(&argc, argv); |
| ::testing::UnitTest::GetInstance()->listeners().Append(new TestExecutionTracer()); |
| ABinderProcess_setThreadPoolMaxThreadCount(1); |
| ABinderProcess_startThreadPool(); |
| return RUN_ALL_TESTS(); |
| } |