MediaMetrics: Add AnalyticsActions and AnalyticsState
Factor out common analytics code from audio specifics.
Allow for saving and clearing analytics state if audioserver crashes.
Test: atest mediametrics_tests
Test: instrumented check on audioserver restart
Bug: 138583596
Change-Id: I1073f3ef95f44a383a7f14b0c2ea6f978a84ee24
diff --git a/services/mediametrics/AnalyticsActions.h b/services/mediametrics/AnalyticsActions.h
new file mode 100644
index 0000000..5568c91
--- /dev/null
+++ b/services/mediametrics/AnalyticsActions.h
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#pragma once
+
+#include <media/MediaMetricsItem.h>
+#include <mutex>
+
+namespace android::mediametrics {
+
+/**
+ * AnalyticsActions consists of a map of pairs <trigger, action> which
+ * are evaluated for a given incoming MediaMetrics item.
+ *
+ * A vector of Actions are returned from getActionsForItem() which
+ * should be executed outside of any locks.
+ *
+ * Mediametrics assumes weak consistency, which is fine as the analytics database
+ * is generally strictly increasing in size (until gc removes values that are
+ * supposedly no longer needed).
+ */
+
+class AnalyticsActions {
+public:
+
+ using Elem = mediametrics::Item::Prop::Elem;
+ /**
+ * Trigger: a pair consisting of
+ * std::string: A wildcard url specifying a property in the item,
+ * where '*' indicates 0 or more arbitrary characters
+ * for the item key match.
+ * Elem: A value that needs to match exactly.
+ *
+ * Trigger is used in a map sort; default less with std::string as primary key.
+ * The wildcard accepts a string with '*' as being 0 or more arbitrary
+ * characters for the item key match. A wildcard is preferred over general
+ * regexp for simple fast lookup.
+ *
+ * TODO: incorporate a regexp option.
+ */
+ using Trigger = std::pair<std::string, Elem>;
+
+ /**
+ * Function: The function to be executed.
+ */
+ using Function = std::function<
+ void(const std::shared_ptr<const mediametrics::Item>& item)>;
+
+ /**
+ * Action: An action to execute. This is a shared pointer to Function.
+ */
+ using Action = std::shared_ptr<Function>;
+
+ /**
+ * Adds a new action.
+ *
+ * \param url references a property in the item with wildcards
+ * \param value references a value (cast to Elem automatically)
+ * so be careful of the type. It must be one of
+ * the types acceptable to Elem.
+ * \param action is a function or lambda to execute if the url matches value
+ * in the item.
+ */
+ template <typename T, typename U, typename A>
+ void addAction(T&& url, U&& value, A&& action) {
+ std::lock_guard l(mLock);
+ mFilters[ { std::forward<T>(url), std::forward<U>(value) } ]
+ = std::forward<A>(action);
+ }
+
+ // TODO: remove an action.
+
+ /**
+ * Get all the actions triggered for a particular item.
+ *
+ * \param item to be analyzed for actions.
+ */
+ std::vector<Action>
+ getActionsForItem(const std::shared_ptr<const mediametrics::Item>& item) {
+ std::vector<Action> actions;
+ std::lock_guard l(mLock);
+
+ // Essentially the code looks like this:
+ /*
+ for (auto &[trigger, action] : mFilters) {
+ if (isMatch(trigger, item)) {
+ actions.push_back(action);
+ }
+ }
+ */
+
+ // Optimization: there should only be one match for a non-wildcard url.
+ auto it = mFilters.upper_bound( {item->getKey(), std::monostate{} });
+ if (it != mFilters.end()) {
+ const auto &[trigger, action] = *it;
+ if (isMatch(trigger, item)) {
+ actions.push_back(action);
+ }
+ }
+
+ // Optimization: for wildcard URLs we go backwards until there is no
+ // match with the prefix before the wildcard.
+ while (it != mFilters.begin()) { // this walks backwards, cannot start at begin.
+ const auto &[trigger, action] = *--it; // look backwards
+ int ret = isWildcardMatch(trigger, item);
+ if (ret == mediametrics::Item::RECURSIVE_WILDCARD_CHECK_MATCH_FOUND) {
+ actions.push_back(action); // match found.
+ } else if (ret == mediametrics::Item::RECURSIVE_WILDCARD_CHECK_NO_MATCH_NO_WILDCARD) {
+ break; // no match before wildcard.
+ }
+ // a wildcard was encountered when matching prefix, so we should check again.
+ }
+ return actions;
+ }
+
+private:
+
+ static inline bool isMatch(const Trigger& trigger,
+ const std::shared_ptr<const mediametrics::Item>& item) {
+ const auto& [key, elem] = trigger;
+ if (!startsWith(key, item->getKey())) return false;
+ // The trigger key is in format (item key).propName, so + 1 skips '.' delimeter.
+ const char *propName = key.c_str() + item->getKey().size() + 1;
+ return item->hasPropElem(propName, elem);
+ }
+
+ static inline int isWildcardMatch(const Trigger& trigger,
+ const std::shared_ptr<const mediametrics::Item>& item) {
+ const auto& [key, elem] = trigger;
+ return item->recursiveWildcardCheckElem(key.c_str(), elem);
+ }
+
+ mutable std::mutex mLock;
+ std::map<Trigger, Action> mFilters; // GUARDED_BY mLock
+};
+
+} // namespace android::mediametrics
diff --git a/services/mediametrics/AnalyticsState.h b/services/mediametrics/AnalyticsState.h
new file mode 100644
index 0000000..4711bb6
--- /dev/null
+++ b/services/mediametrics/AnalyticsState.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#pragma once
+
+#include "TimeMachine.h"
+#include "TransactionLog.h"
+
+namespace android::mediametrics {
+
+/**
+ * AnalyticsState consists of a TimeMachine and TransactionLog for a set
+ * of MediaMetrics Items.
+ *
+ * One can add new Items with the submit() method.
+ *
+ * The AnalyticsState may be cleared or duplicated to preserve state after crashes
+ * in services are detected.
+ *
+ * As its members may not be moveable due to mutexes, we use this encapsulation
+ * with a shared pointer in order to save it or duplicate it.
+ */
+class AnalyticsState {
+public:
+ /**
+ * Returns success if AnalyticsState accepts the item.
+ *
+ * A trusted source can create a new key, an untrusted source
+ * can only modify the key if the uid will match that authorized
+ * on the existing key.
+ *
+ * \param item the item to be submitted.
+ * \param isTrusted whether the transaction comes from a trusted source.
+ * In this case, a trusted source is verified by binder
+ * UID to be a system service by MediaMetrics service.
+ * Do not use true if you haven't really checked!
+ *
+ * \return NO_ERROR on success or
+ * PERMISSION_DENIED if the item cannot be put into the AnalyticsState.
+ */
+ status_t submit(const std::shared_ptr<const mediametrics::Item>& item, bool isTrusted) {
+ return mTimeMachine.put(item, isTrusted) ?: mTransactionLog.put(item);
+ }
+
+ /**
+ * Returns a pair consisting of the dump string, and the number of lines in the string.
+ *
+ * The number of lines in the returned pair is used as an optimization
+ * for subsequent line limiting.
+ *
+ * The TimeMachine and the TransactionLog are dumped separately under
+ * different locks, so may not be 100% consistent with the last data
+ * delivered.
+ *
+ * \param lines the maximum number of lines in the string returned.
+ */
+ std::pair<std::string, int32_t> dump(int32_t lines = INT32_MAX) const {
+ std::stringstream ss;
+ int32_t ll = lines;
+
+ if (ll > 0) {
+ ss << "TransactionLog:\n";
+ --ll;
+ }
+ if (ll > 0) {
+ auto [s, l] = mTransactionLog.dump(ll);
+ ss << s;
+ ll -= l;
+ }
+ if (ll > 0) {
+ ss << "TimeMachine:\n";
+ --ll;
+ }
+ if (ll > 0) {
+ auto [s, l] = mTimeMachine.dump(ll);
+ ss << s;
+ ll -= l;
+ }
+ return { ss.str(), lines - ll };
+ }
+
+ /**
+ * Clears the AnalyticsState.
+ */
+ void clear() {
+ mTimeMachine.clear();
+ mTransactionLog.clear();
+ }
+
+private:
+ // Note: TimeMachine and TransactionLog are individually locked.
+ // Access to these objects under multiple threads will be weakly synchronized,
+ // which is acceptable as modifications only increase the history (or with GC,
+ // eliminates very old history).
+
+ TimeMachine mTimeMachine;
+ TransactionLog mTransactionLog;
+};
+
+} // namespace android::mediametrics
diff --git a/services/mediametrics/AudioAnalytics.cpp b/services/mediametrics/AudioAnalytics.cpp
index fffe517..f7ee051 100644
--- a/services/mediametrics/AudioAnalytics.cpp
+++ b/services/mediametrics/AudioAnalytics.cpp
@@ -27,6 +27,22 @@
AudioAnalytics::AudioAnalytics()
{
ALOGD("%s", __func__);
+
+ // Add action to save AnalyticsState if audioserver is restarted.
+ // This triggers on an item of "audio.flinger"
+ // with a property "event" set to "AudioFlinger" (the constructor).
+ mActions.addAction(
+ "audio.flinger.event",
+ std::string("AudioFlinger"),
+ std::make_shared<AnalyticsActions::Function>(
+ [this](const std::shared_ptr<const android::mediametrics::Item> &){
+ ALOGW("Audioflinger() constructor event detected");
+ mPreviousAnalyticsState.set(std::make_shared<AnalyticsState>(
+ *mAnalyticsState.get()));
+ // Note: get returns shared_ptr temp, whose lifetime is extended
+ // to end of full expression.
+ mAnalyticsState->clear(); // TODO: filter the analytics state.
+ }));
}
AudioAnalytics::~AudioAnalytics()
@@ -37,11 +53,14 @@
status_t AudioAnalytics::submit(
const std::shared_ptr<const mediametrics::Item>& item, bool isTrusted)
{
- if (startsWith(item->getKey(), "audio.")) {
- return mTimeMachine.put(item, isTrusted)
- ?: mTransactionLog.put(item);
- }
- return BAD_VALUE;
+ if (!startsWith(item->getKey(), "audio.")) return BAD_VALUE;
+ status_t status = mAnalyticsState->submit(item, isTrusted);
+ if (status != NO_ERROR) return status; // may not be permitted.
+
+ // Only if the item was successfully submitted (permission)
+ // do we check triggered actions.
+ checkActions(item);
+ return NO_ERROR;
}
std::pair<std::string, int32_t> AudioAnalytics::dump(int32_t lines) const
@@ -50,24 +69,29 @@
int32_t ll = lines;
if (ll > 0) {
- ss << "TransactionLog:\n";
- --ll;
- }
- if (ll > 0) {
- auto [s, l] = mTransactionLog.dump(ll);
+ auto [s, l] = mAnalyticsState->dump(ll);
ss << s;
ll -= l;
}
if (ll > 0) {
- ss << "TimeMachine:\n";
+ ss << "Prior audioserver state:\n";
--ll;
}
if (ll > 0) {
- auto [s, l] = mTimeMachine.dump(ll);
+ auto [s, l] = mPreviousAnalyticsState->dump(ll);
ss << s;
ll -= l;
}
return { ss.str(), lines - ll };
}
+void AudioAnalytics::checkActions(const std::shared_ptr<const mediametrics::Item>& item)
+{
+ auto actions = mActions.getActionsForItem(item); // internally locked.
+ // Execute actions with no lock held.
+ for (const auto& action : actions) {
+ (*action)(item);
+ }
+}
+
} // namespace android
diff --git a/services/mediametrics/AudioAnalytics.h b/services/mediametrics/AudioAnalytics.h
index b931258..be885ec 100644
--- a/services/mediametrics/AudioAnalytics.h
+++ b/services/mediametrics/AudioAnalytics.h
@@ -16,8 +16,9 @@
#pragma once
-#include "TimeMachine.h"
-#include "TransactionLog.h"
+#include "AnalyticsActions.h"
+#include "AnalyticsState.h"
+#include "Wrap.h"
namespace android::mediametrics {
@@ -27,7 +28,6 @@
AudioAnalytics();
~AudioAnalytics();
- // TODO: update with conditions for keys.
/**
* Returns success if AudioAnalytics recognizes item.
*
@@ -42,6 +42,10 @@
* In this case, a trusted source is verified by binder
* UID to be a system service by MediaMetrics service.
* Do not use true if you haven't really checked!
+ *
+ * \return NO_ERROR on success,
+ * PERMISSION_DENIED if the item cannot be put into the AnalyticsState,
+ * BAD_VALUE if the item key does not start with "audio.".
*/
status_t submit(const std::shared_ptr<const mediametrics::Item>& item, bool isTrusted);
@@ -60,9 +64,22 @@
std::pair<std::string, int32_t> dump(int32_t lines = INT32_MAX) const;
private:
- // The following are locked internally
- TimeMachine mTimeMachine;
- TransactionLog mTransactionLog;
+
+ /**
+ * Checks for any pending actions for a particular item.
+ *
+ * \param item to check against the current AnalyticsActions.
+ */
+ void checkActions(const std::shared_ptr<const mediametrics::Item>& item);
+
+ // Actions is individually locked
+ AnalyticsActions mActions;
+
+ // AnalyticsState is individually locked, and we use SharedPtrWrap
+ // to allow safe access even if the shared pointer changes underneath.
+
+ SharedPtrWrap<AnalyticsState> mAnalyticsState;
+ SharedPtrWrap<AnalyticsState> mPreviousAnalyticsState;
};
} // namespace android::mediametrics
diff --git a/services/mediametrics/MediaMetricsService.cpp b/services/mediametrics/MediaMetricsService.cpp
index b5bdd6f..3b95f7a 100644
--- a/services/mediametrics/MediaMetricsService.cpp
+++ b/services/mediametrics/MediaMetricsService.cpp
@@ -291,7 +291,11 @@
}
// TODO: maybe consider a better way of dumping audio analytics info.
constexpr int32_t linesToDump = 1000;
- result.append(mAudioAnalytics.dump(linesToDump).first.c_str());
+ auto [ dumpString, lines ] = mAudioAnalytics.dump(linesToDump);
+ result.append(dumpString.c_str());
+ if (lines == linesToDump) {
+ result.append("-- some lines may be truncated --\n");
+ }
}
write(fd, result.string(), result.size());
diff --git a/services/mediametrics/Wrap.h b/services/mediametrics/Wrap.h
new file mode 100644
index 0000000..99963fb
--- /dev/null
+++ b/services/mediametrics/Wrap.h
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2020 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.
+ */
+
+#pragma once
+
+#include <memory>
+#include <mutex>
+
+namespace android::mediametrics {
+
+/**
+ * Wraps a shared-ptr for which member access through operator->() behaves
+ * as if the shared-ptr is atomically copied and then (without a lock) -> called.
+ *
+ * See related C++ 20:
+ * https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic2
+ *
+ * EXAMPLE:
+ *
+ * SharedPtrWrap<T> t{};
+ *
+ * thread1() {
+ * t->func(); // safely executes either the original t or the one created by thread2.
+ * }
+ *
+ * thread2() {
+ * t.set(std::make_shared<T>()); // overwrites the original t.
+ * }
+ */
+template <typename T>
+class SharedPtrWrap {
+ mutable std::mutex mLock;
+ std::shared_ptr<T> mPtr;
+
+public:
+ template <typename... Args>
+ explicit SharedPtrWrap(Args&&... args)
+ : mPtr(std::make_shared<T>(std::forward<Args>(args)...))
+ {}
+
+ /**
+ * Gets the current shared pointer. This must return a value, not a reference.
+ *
+ * For compatibility with existing shared_ptr, we do not pass back a
+ * shared_ptr<const T> for the const getter.
+ */
+ std::shared_ptr<T> get() const {
+ std::lock_guard lock(mLock);
+ return mPtr;
+ }
+
+ /**
+ * Sets the current shared pointer, returning the previous shared pointer.
+ */
+ std::shared_ptr<T> set(std::shared_ptr<T> ptr) { // pass by value as we use swap.
+ std::lock_guard lock(mLock);
+ std::swap(ptr, mPtr);
+ return ptr;
+ }
+
+ /**
+ * Returns a shared pointer value representing T at the instant of time when
+ * the call executes. The lifetime of the shared pointer will
+ * be extended as we are returning an instance of the shared_ptr
+ * not a reference to it. The destructor to the returned shared_ptr
+ * will be called sometime after the expression including the member function or
+ * the member variable is evaluated. Do not change to a reference!
+ */
+
+ // For compatibility with existing shared_ptr, we do not pass back a
+ // shared_ptr<const T> for the const operator pointer access.
+ std::shared_ptr<T> operator->() const {
+ return get();
+ }
+ /**
+ * We do not overload operator*() as the reference is not stable if the
+ * lock is not held.
+ */
+};
+
+/**
+ * Wraps member access to the class T by a lock.
+ *
+ * The object T is constructed within the LockWrap to guarantee
+ * locked access at all times. When T's methods are accessed through ->,
+ * a monitor style lock is obtained to prevent multiple threads from executing
+ * methods in the object T at the same time.
+ * Suggested by Kevin R.
+ *
+ * EXAMPLE:
+ *
+ * // Accumulator class which is very slow, requires locking for multiple threads.
+ *
+ * class Accumulator {
+ * int32_t value_ = 0;
+ * public:
+ * void add(int32_t incr) {
+ * const int32_t temp = value_;
+ * sleep(0); // yield
+ * value_ = temp + incr;
+ * }
+ * int32_t get() { return value_; }
+ * };
+ *
+ * // We use LockWrap on Accumulator to have safe multithread access.
+ * android::mediametrics::LockWrap<Accumulator> a{}; // locked accumulator succeeds
+ *
+ * // Conversely, the following line fails:
+ * // auto a = std::make_shared<Accumulator>(); // this fails, only 50% adds atomic.
+ *
+ * constexpr size_t THREADS = 100;
+ * constexpr size_t ITERATIONS = 10;
+ * constexpr int32_t INCREMENT = 1;
+ *
+ * // Test by generating multiple threads, all adding simultaneously.
+ * std::vector<std::future<void>> threads(THREADS);
+ * for (size_t i = 0; i < THREADS; ++i) {
+ * threads.push_back(std::async(std::launch::async, [&] {
+ * for (size_t j = 0; j < ITERATIONS; ++j) {
+ * a->add(INCREMENT); // add needs locked access here.
+ * }
+ * }));
+ * }
+ * threads.clear();
+ *
+ * // If the add operations are not atomic, value will be smaller than expected.
+ * ASSERT_EQ(INCREMENT * THREADS * ITERATIONS, (size_t)a->get());
+ *
+ */
+template <typename T>
+class LockWrap {
+ /**
+ * Holding class that keeps the pointer and the lock.
+ *
+ * We return this holding class from operator->() to keep the lock until the
+ * method function or method variable access is completed.
+ */
+ class LockedPointer {
+ friend LockWrap;
+ LockedPointer(T *t, std::mutex *lock)
+ : mT(t), mLock(*lock) {}
+ T* const mT;
+ std::lock_guard<std::mutex> mLock;
+ public:
+ const T* operator->() const {
+ return mT;
+ }
+ T* operator->() {
+ return mT;
+ }
+ };
+
+ mutable std::mutex mLock;
+ mutable T mT;
+
+public:
+ template <typename... Args>
+ explicit LockWrap(Args&&... args) : mT(std::forward<Args>(args)...) {}
+
+ const LockedPointer operator->() const {
+ return LockedPointer(&mT, &mLock);
+ }
+ LockedPointer operator->() {
+ return LockedPointer(&mT, &mLock);
+ }
+
+ // @TestApi
+ bool isLocked() const {
+ if (mLock.try_lock()) {
+ mLock.unlock();
+ return false; // we were able to get the lock.
+ }
+ return true; // we were NOT able to get the lock.
+ }
+};
+
+} // namespace android::mediametrics
diff --git a/services/mediametrics/tests/mediametrics_tests.cpp b/services/mediametrics/tests/mediametrics_tests.cpp
index 90a8565..dea625c 100644
--- a/services/mediametrics/tests/mediametrics_tests.cpp
+++ b/services/mediametrics/tests/mediametrics_tests.cpp
@@ -52,6 +52,117 @@
ASSERT_EQ(true, check);
}
+TEST(mediametrics_tests, shared_ptr_wrap) {
+ // Test shared pointer wrap with simple access
+ android::mediametrics::SharedPtrWrap<std::string> s("123");
+ ASSERT_EQ('1', s->at(0));
+ ASSERT_EQ('2', s->at(1));
+ s->push_back('4');
+ ASSERT_EQ('4', s->at(3));
+
+ const android::mediametrics::SharedPtrWrap<std::string> s2("345");
+ ASSERT_EQ('3', s2->operator[](0)); // s2[0] == '3'
+ // we allow modification through a const shared pointer wrap
+ // for compatibility with shared_ptr.
+ s2->push_back('6');
+ ASSERT_EQ('6', s2->operator[](3)); // s2[3] == '6'
+
+ android::mediametrics::SharedPtrWrap<std::string> s3("");
+ s3.set(std::make_shared<std::string>("abc"));
+ ASSERT_EQ('b', s3->operator[](1)); // s2[1] = 'b';
+
+ // Use Thunk to check whether the destructor was called prematurely
+ // when setting the shared ptr wrap in the middle of a method.
+
+ class Thunk {
+ std::function<void(int)> mF;
+ const int mFinal;
+
+ public:
+ Thunk(decltype(mF) f, int final) : mF(f), mFinal(final) {}
+ ~Thunk() { mF(mFinal); }
+ void thunk(int value) { mF(value); }
+ };
+
+ int counter = 0;
+ android::mediametrics::SharedPtrWrap<Thunk> s4(
+ [&](int value) {
+ s4.set(std::make_shared<Thunk>([](int){}, 0)); // recursively set s4 while in s4.
+ ++counter;
+ ASSERT_EQ(value, counter); // on thunk() value is 1, on destructor this is 2.
+ }, 2);
+
+ // This will fail if the shared ptr wrap doesn't hold a ref count during method access.
+ s4->thunk(1);
+}
+
+TEST(mediametrics_tests, lock_wrap) {
+ // Test lock wrap with simple access
+ android::mediametrics::LockWrap<std::string> s("123");
+ ASSERT_EQ('1', s->at(0));
+ ASSERT_EQ('2', s->at(1));
+ s->push_back('4');
+ ASSERT_EQ('4', s->at(3));
+
+ const android::mediametrics::LockWrap<std::string> s2("345");
+ ASSERT_EQ('3', s2->operator[](0)); // s2[0] == '3'
+ // note: we can't modify s2 due to const, s2->push_back('6');
+
+ android::mediametrics::LockWrap<std::string> s3("");
+ s3->operator=("abc");
+ ASSERT_EQ('b', s3->operator[](1)); // s2[1] = 'b';
+
+ // Use Thunk to check whether we have the lock when calling a method through LockWrap.
+
+ class Thunk {
+ std::function<void()> mF;
+
+ public:
+ Thunk(decltype(mF) f) : mF(f) {}
+ void thunk() { mF(); }
+ };
+
+ android::mediametrics::LockWrap<Thunk> s4([&]{
+ ASSERT_EQ(true, s4.isLocked()); // we must be locked when thunk() is called.
+ });
+
+ // This will fail if we are not locked during method access.
+ s4->thunk();
+}
+
+TEST(mediametrics_tests, lock_wrap_multithread) {
+ class Accumulator {
+ int32_t value_ = 0;
+ public:
+ void add(int32_t incr) {
+ const int32_t temp = value_;
+ sleep(0); // yield
+ value_ = temp + incr;
+ }
+ int32_t get() { return value_; }
+ };
+
+ android::mediametrics::LockWrap<Accumulator> a{}; // locked accumulator succeeds
+ // auto a = std::make_shared<Accumulator>(); // this fails, only 50% adds atomic.
+
+ constexpr size_t THREADS = 100;
+ constexpr size_t ITERATIONS = 10;
+ constexpr int32_t INCREMENT = 1;
+
+ std::vector<std::future<void>> threads(THREADS);
+ for (size_t i = 0; i < THREADS; ++i) {
+ threads.push_back(std::async(std::launch::async, [&] {
+ for (size_t j = 0; j < ITERATIONS; ++j) {
+ a->add(INCREMENT);
+ }
+ }));
+ }
+ threads.clear();
+
+ // If the add operations are not atomic, value will be smaller than expected.
+ ASSERT_EQ(INCREMENT * THREADS * ITERATIONS, (size_t)a->get());
+}
+
TEST(mediametrics_tests, instantiate) {
sp mediaMetrics = new MediaMetricsService();
status_t status;
@@ -592,6 +703,61 @@
ASSERT_EQ((size_t)2, transactionLog.size());
}
+TEST(mediametrics_tests, analytics_actions) {
+ mediametrics::AnalyticsActions analyticsActions;
+ bool action1 = false;
+ bool action2 = false;
+ bool action3 = false;
+ bool action4 = false;
+
+ // check to see whether various actions have been matched.
+ analyticsActions.addAction(
+ "audio.flinger.event",
+ std::string("AudioFlinger"),
+ std::make_shared<mediametrics::AnalyticsActions::Function>(
+ [&](const std::shared_ptr<const android::mediametrics::Item> &) {
+ action1 = true;
+ }));
+
+ analyticsActions.addAction(
+ "audio.*.event",
+ std::string("AudioFlinger"),
+ std::make_shared<mediametrics::AnalyticsActions::Function>(
+ [&](const std::shared_ptr<const android::mediametrics::Item> &) {
+ action2 = true;
+ }));
+
+ analyticsActions.addAction("audio.fl*n*g*r.event",
+ std::string("AudioFlinger"),
+ std::make_shared<mediametrics::AnalyticsActions::Function>(
+ [&](const std::shared_ptr<const android::mediametrics::Item> &) {
+ action3 = true;
+ }));
+
+ analyticsActions.addAction("audio.fl*gn*r.event",
+ std::string("AudioFlinger"),
+ std::make_shared<mediametrics::AnalyticsActions::Function>(
+ [&](const std::shared_ptr<const android::mediametrics::Item> &) {
+ action4 = true;
+ }));
+
+ // make a test item
+ auto item = std::make_shared<mediametrics::Item>("audio.flinger");
+ (*item).set("event", "AudioFlinger");
+
+ // get the actions and execute them
+ auto actions = analyticsActions.getActionsForItem(item);
+ for (const auto& action : actions) {
+ action->operator()(item);
+ }
+
+ // The following should match.
+ ASSERT_EQ(true, action1);
+ ASSERT_EQ(true, action2);
+ ASSERT_EQ(true, action3);
+ ASSERT_EQ(false, action4); // audio.fl*gn*r != audio.flinger
+}
+
TEST(mediametrics_tests, audio_analytics_permission) {
auto item = std::make_shared<mediametrics::Item>("audio.1");
(*item).set("one", (int32_t)1)
@@ -614,14 +780,14 @@
// TODO: Verify contents of AudioAnalytics.
// Currently there is no getter API in AudioAnalytics besides dump.
- ASSERT_EQ(4, audioAnalytics.dump(1000).second /* lines */);
+ ASSERT_EQ(9, audioAnalytics.dump(1000).second /* lines */);
ASSERT_EQ(NO_ERROR, audioAnalytics.submit(item, true /* isTrusted */));
// untrusted entities can add to an existing key
ASSERT_EQ(NO_ERROR, audioAnalytics.submit(item2, false /* isTrusted */));
// Check that we have some info in the dump.
- ASSERT_LT(4, audioAnalytics.dump(1000).second /* lines */);
+ ASSERT_LT(9, audioAnalytics.dump(1000).second /* lines */);
}
TEST(mediametrics_tests, audio_analytics_dump) {