MediaMetrics: Add device-based actions for Bluetooth
Log summary group statistics by device.
Test: adb shell dumpsys media.metrics
Bug: 149850236
Change-Id: I31fef89fa020de175e59784049ef4741914f3a2c
diff --git a/services/mediametrics/AnalyticsActions.h b/services/mediametrics/AnalyticsActions.h
index 0151134..897e567 100644
--- a/services/mediametrics/AnalyticsActions.h
+++ b/services/mediametrics/AnalyticsActions.h
@@ -78,8 +78,8 @@
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);
+ mFilters.emplace(Trigger{ std::forward<T>(url), std::forward<U>(value) },
+ std::forward<A>(action));
}
// TODO: remove an action.
@@ -94,36 +94,15 @@
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)) {
+ for (const auto &[trigger, action] : mFilters) {
+ if (isWildcardMatch(trigger, item) ==
+ mediametrics::Item::RECURSIVE_WILDCARD_CHECK_MATCH_FOUND) {
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.
- }
+ // TODO: Optimize for prefix search and wildcarding.
+
return actions;
}
@@ -145,7 +124,9 @@
}
mutable std::mutex mLock;
- std::map<Trigger, Action> mFilters GUARDED_BY(mLock);
+
+ using FilterType = std::multimap<Trigger, Action>;
+ FilterType mFilters GUARDED_BY(mLock);
};
} // namespace android::mediametrics
diff --git a/services/mediametrics/Android.bp b/services/mediametrics/Android.bp
index fb4022e..8e5bc79 100644
--- a/services/mediametrics/Android.bp
+++ b/services/mediametrics/Android.bp
@@ -76,5 +76,6 @@
"-Wall",
"-Werror",
"-Wextra",
+ "-Wthread-safety",
],
}
diff --git a/services/mediametrics/AudioAnalytics.cpp b/services/mediametrics/AudioAnalytics.cpp
index 3f9a42f..fe3a34d 100644
--- a/services/mediametrics/AudioAnalytics.cpp
+++ b/services/mediametrics/AudioAnalytics.cpp
@@ -19,8 +19,12 @@
#include <utils/Log.h>
#include "AudioAnalytics.h"
+#include "MediaMetricsService.h" // package info
+#include <audio_utils/clock.h> // clock conversions
+#include <statslog.h> // statsd
-#include <audio_utils/clock.h> // clock conversions
+// Enable for testing of delivery to statsd
+// #define STATSD
namespace android::mediametrics {
@@ -87,11 +91,53 @@
// report this for Bluetooth
}
}));
+
+ // Handle device use thread statistics
+ mActions.addAction(
+ AMEDIAMETRICS_KEY_PREFIX_AUDIO_THREAD "*." AMEDIAMETRICS_PROP_EVENT,
+ std::string(AMEDIAMETRICS_PROP_EVENT_VALUE_ENDAUDIOINTERVALGROUP),
+ std::make_shared<AnalyticsActions::Function>(
+ [this](const std::shared_ptr<const android::mediametrics::Item> &item){
+ mDeviceUse.endAudioIntervalGroup(item, false /* isTrack */);
+ }));
+
+ // Handle device use track statistics
+ mActions.addAction(
+ AMEDIAMETRICS_KEY_PREFIX_AUDIO_TRACK "*." AMEDIAMETRICS_PROP_EVENT,
+ std::string(AMEDIAMETRICS_PROP_EVENT_VALUE_ENDAUDIOINTERVALGROUP),
+ std::make_shared<AnalyticsActions::Function>(
+ [this](const std::shared_ptr<const android::mediametrics::Item> &item){
+ mDeviceUse.endAudioIntervalGroup(item, true /* isTrack */);
+ }));
+
+ // Handle device routing statistics
+
+ // We track connections (not disconnections) for the time to connect.
+ // TODO: consider BT requests in their A2dp service
+ // AudioManager.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent
+ // AudioDeviceBroker.postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent
+ // AudioDeviceBroker.postA2dpActiveDeviceChange
+ mActions.addAction(
+ "audio.device.a2dp.state",
+ std::string("connected"),
+ std::make_shared<AnalyticsActions::Function>(
+ [this](const std::shared_ptr<const android::mediametrics::Item> &item){
+ mDeviceConnection.a2dpConnected(item);
+ }));
+ // If audio is active, we expect to see a createAudioPatch after the device is connected.
+ mActions.addAction(
+ AMEDIAMETRICS_KEY_PREFIX_AUDIO_THREAD "*." AMEDIAMETRICS_PROP_EVENT,
+ std::string("createAudioPatch"),
+ std::make_shared<AnalyticsActions::Function>(
+ [this](const std::shared_ptr<const android::mediametrics::Item> &item){
+ mDeviceConnection.createPatch(item);
+ }));
}
AudioAnalytics::~AudioAnalytics()
{
ALOGD("%s", __func__);
+ mTimedAction.quit(); // ensure no deferred access during destructor.
}
status_t AudioAnalytics::submit(
@@ -151,4 +197,220 @@
return std::string(AMEDIAMETRICS_KEY_PREFIX_AUDIO_THREAD) + std::to_string(threadId_int32);
}
+// DeviceUse helper class.
+void AudioAnalytics::DeviceUse::endAudioIntervalGroup(
+ const std::shared_ptr<const android::mediametrics::Item> &item, bool isTrack) const {
+ const std::string& key = item->getKey();
+ const std::string id = key.substr(
+ (isTrack ? sizeof(AMEDIAMETRICS_KEY_PREFIX_AUDIO_TRACK)
+ : sizeof(AMEDIAMETRICS_KEY_PREFIX_AUDIO_THREAD))
+ - 1);
+ // deliver statistics
+ int64_t deviceTimeNs = 0;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_DEVICETIMENS, &deviceTimeNs);
+ std::string encoding;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_ENCODING, &encoding);
+ int32_t frameCount = 0;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_FRAMECOUNT, &frameCount);
+ int32_t intervalCount = 0;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_INTERVALCOUNT, &intervalCount);
+ std::string outputDevices;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_OUTPUTDEVICES, &outputDevices);
+ int32_t sampleRate = 0;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_SAMPLERATE, &sampleRate);
+ int32_t underrun = 0;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_UNDERRUN, &underrun);
+
+ // Get connected device name if from bluetooth.
+ bool isBluetooth = false;
+ std::string name;
+ if (outputDevices.find("AUDIO_DEVICE_OUT_BLUETOOTH") != std::string::npos) {
+ isBluetooth = true;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ "audio.device.bt_a2dp", AMEDIAMETRICS_PROP_NAME, &name);
+ }
+
+ // We may have several devices. We only list the first device.
+ // TODO: consider whether we should list all the devices separated by |
+ std::string firstDevice = "unknown";
+ auto devaddrvec = MediaMetricsService::getDeviceAddressPairs(outputDevices);
+ if (devaddrvec.size() != 0) {
+ firstDevice = devaddrvec[0].first;
+ // DO NOT show the address.
+ }
+
+ if (isTrack) {
+ std::string callerName;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_CALLERNAME, &callerName);
+ std::string contentType;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_CONTENTTYPE, &contentType);
+ double deviceLatencyMs = 0.;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_DEVICELATENCYMS, &deviceLatencyMs);
+ double deviceStartupMs = 0.;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_DEVICESTARTUPMS, &deviceStartupMs);
+ double deviceVolume = 0.;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_DEVICEVOLUME, &deviceVolume);
+ std::string packageName;
+ int64_t versionCode = 0;
+ int32_t uid = -1;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_ALLOWUID, &uid);
+ if (uid != -1) {
+ std::tie(packageName, versionCode) =
+ MediaMetricsService::getSanitizedPackageNameAndVersionCode(uid);
+ }
+ double playbackPitch = 0.;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_PLAYBACK_PITCH, &playbackPitch);
+ double playbackSpeed = 0.;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_PLAYBACK_SPEED, &playbackSpeed);
+ int32_t selectedDeviceId = 0;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_SELECTEDDEVICEID, &selectedDeviceId);
+
+ std::string usage;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_USAGE, &usage);
+
+ ALOGD("(key=%s) id:%s endAudioIntervalGroup device:%s name:%s "
+ "deviceTimeNs:%lld encoding:%s frameCount:%d intervalCount:%d "
+ "sampleRate:%d underrun:%d "
+ "callerName:%s contentType:%s "
+ "deviceLatencyMs:%lf deviceStartupMs:%lf deviceVolume:%lf"
+ "packageName:%s playbackPitch:%lf playbackSpeed:%lf "
+ "selectedDevceId:%d usage:%s",
+ key.c_str(), id.c_str(), firstDevice.c_str(), name.c_str(),
+ (long long)deviceTimeNs, encoding.c_str(), frameCount, intervalCount,
+ sampleRate, underrun,
+ callerName.c_str(), contentType.c_str(),
+ deviceLatencyMs, deviceStartupMs, deviceVolume,
+ packageName.c_str(), playbackPitch, playbackSpeed,
+ selectedDeviceId, usage.c_str());
+#ifdef STATSD
+ if (mAudioAnalytics.mDeliverStatistics) {
+ (void)android::util::stats_write(
+ android::util::MEDIAMETRICS_AUDIOTRACKDEVICEUSAGE_REPORTED
+ /* timestamp, */
+ /* mediaApexVersion, */
+ , firstDevice.c_str()
+ , name.c_str()
+ , deviceTimeNs
+ , encoding.c_str()
+ , frameCount
+ , intervalCount
+ , sampleRate
+ , underrun
+
+ , packageName.c_str()
+ , (float)deviceLatencyMs
+ , (float)deviceStartupMs
+ , (float)deviceVolume
+ , selectedDeviceId
+ , usage.c_str()
+ , contentType.c_str()
+ , callerName.c_str()
+ );
+ }
+#endif
+ } else {
+
+ std::string flags;
+ mAudioAnalytics.mAnalyticsState->timeMachine().get(
+ key, AMEDIAMETRICS_PROP_FLAGS, &flags);
+
+ ALOGD("(key=%s) id:%s endAudioIntervalGroup device:%s name:%s "
+ "deviceTimeNs:%lld encoding:%s frameCount:%d intervalCount:%d "
+ "sampleRate:%d underrun:%d "
+ "flags:%s",
+ key.c_str(), id.c_str(), firstDevice.c_str(), name.c_str(),
+ (long long)deviceTimeNs, encoding.c_str(), frameCount, intervalCount,
+ sampleRate, underrun,
+ flags.c_str());
+#ifdef STATSD
+ if (mAudioAnalytics.mDeliverStatistics) {
+ (void)android::util::stats_write(
+ android::util::MEDIAMETRICS_AUDIOTHREADDEVICEUSAGE_REPORTED
+ /* timestamp, */
+ /* mediaApexVersion, */
+ , firstDevice.c_str()
+ , name.c_str()
+ , deviceTimeNs
+ , encoding.c_str()
+ , frameCount
+ , intervalCount
+ , sampleRate
+ , underrun
+ );
+ }
+#endif
+ }
+
+ // Report this as needed.
+ if (isBluetooth) {
+ // report this for Bluetooth
+ }
+}
+
+// DeviceConnection helper class.
+void AudioAnalytics::DeviceConnection::a2dpConnected(
+ const std::shared_ptr<const android::mediametrics::Item> &item) {
+ const std::string& key = item->getKey();
+
+ const int64_t connectedAtNs = item->getTimestamp();
+ {
+ std::lock_guard l(mLock);
+ mA2dpTimeConnectedNs = connectedAtNs;
+ ++mA2dpConnectedAttempts;
+ }
+ std::string name;
+ item->get(AMEDIAMETRICS_PROP_NAME, &name);
+ ALOGD("(key=%s) a2dp connected device:%s "
+ "connectedAtNs:%lld",
+ key.c_str(), name.c_str(),
+ (long long)connectedAtNs);
+ // Note - we need to be able to cancel a timed event
+ mAudioAnalytics.mTimedAction.postIn(std::chrono::seconds(5), [this](){ expire(); });
+ // This sets the time we were connected. Now we look for the delta in the future.
+}
+
+void AudioAnalytics::DeviceConnection::createPatch(
+ const std::shared_ptr<const android::mediametrics::Item> &item) {
+ std::lock_guard l(mLock);
+ if (mA2dpTimeConnectedNs == 0) return; // ignore
+ const std::string& key = item->getKey();
+ std::string outputDevices;
+ item->get(AMEDIAMETRICS_PROP_OUTPUTDEVICES, &outputDevices);
+ if (outputDevices.find("AUDIO_DEVICE_OUT_BLUETOOTH") != std::string::npos) {
+ // TODO compare address
+ const int64_t timeDiff = item->getTimestamp() - mA2dpTimeConnectedNs;
+ ALOGD("(key=%s) A2DP device connection time: %lld", key.c_str(), (long long)timeDiff);
+ mA2dpTimeConnectedNs = 0; // reset counter.
+ ++mA2dpConnectedSuccesses;
+ }
+}
+
+void AudioAnalytics::DeviceConnection::expire() {
+ std::lock_guard l(mLock);
+ if (mA2dpTimeConnectedNs == 0) return; // ignore
+
+ // An expiration may occur because there is no audio playing.
+ // TODO: disambiguate this case.
+ ALOGD("A2DP device connection expired");
+ ++mA2dpConnectedFailures; // this is not a true failure.
+ mA2dpTimeConnectedNs = 0;
+}
+
} // namespace android
diff --git a/services/mediametrics/AudioAnalytics.h b/services/mediametrics/AudioAnalytics.h
index ba4c3f2..eb9c228 100644
--- a/services/mediametrics/AudioAnalytics.h
+++ b/services/mediametrics/AudioAnalytics.h
@@ -16,8 +16,10 @@
#pragma once
+#include <android-base/thread_annotations.h>
#include "AnalyticsActions.h"
#include "AnalyticsState.h"
+#include "TimedAction.h"
#include "Wrap.h"
namespace android::mediametrics {
@@ -89,6 +91,8 @@
*/
std::string getThreadFromTrack(const std::string& track) const;
+ const bool mDeliverStatistics __unused = true;
+
// Actions is individually locked
AnalyticsActions mActions;
@@ -97,6 +101,54 @@
SharedPtrWrap<AnalyticsState> mAnalyticsState;
SharedPtrWrap<AnalyticsState> mPreviousAnalyticsState;
+
+ TimedAction mTimedAction; // locked internally
+
+ // DeviceUse is a nested class which handles audio device usage accounting.
+ // We define this class at the end to ensure prior variables all properly constructed.
+ // TODO: Track / Thread interaction
+ // TODO: Consider statistics aggregation.
+ class DeviceUse {
+ public:
+ explicit DeviceUse(AudioAnalytics &audioAnalytics) : mAudioAnalytics{audioAnalytics} {}
+
+ // Called every time an endAudioIntervalGroup message is received.
+ void endAudioIntervalGroup(
+ const std::shared_ptr<const android::mediametrics::Item> &item,
+ bool isTrack) const;
+ private:
+ AudioAnalytics &mAudioAnalytics;
+ } mDeviceUse{*this};
+
+ // DeviceConnected is a nested class which handles audio device connection
+ // We define this class at the end to ensure prior variables all properly constructed.
+ // TODO: Track / Thread interaction
+ // TODO: Consider statistics aggregation.
+ class DeviceConnection {
+ public:
+ explicit DeviceConnection(AudioAnalytics &audioAnalytics)
+ : mAudioAnalytics{audioAnalytics} {}
+
+ // Called every time an endAudioIntervalGroup message is received.
+ void a2dpConnected(
+ const std::shared_ptr<const android::mediametrics::Item> &item);
+
+ // Called when we have an AudioFlinger createPatch
+ void createPatch(
+ const std::shared_ptr<const android::mediametrics::Item> &item);
+
+ // When the timer expires.
+ void expire();
+
+ private:
+ AudioAnalytics &mAudioAnalytics;
+
+ mutable std::mutex mLock;
+ int64_t mA2dpTimeConnectedNs GUARDED_BY(mLock) = 0;
+ int32_t mA2dpConnectedAttempts GUARDED_BY(mLock) = 0;
+ int32_t mA2dpConnectedSuccesses GUARDED_BY(mLock) = 0;
+ int32_t mA2dpConnectedFailures GUARDED_BY(mLock) = 0;
+ } mDeviceConnection{*this};
};
} // namespace android::mediametrics
diff --git a/services/mediametrics/MediaMetricsService.cpp b/services/mediametrics/MediaMetricsService.cpp
index 4b84bea..3b3dc3e 100644
--- a/services/mediametrics/MediaMetricsService.cpp
+++ b/services/mediametrics/MediaMetricsService.cpp
@@ -78,6 +78,74 @@
}
}
+/* static */
+std::pair<std::string, int64_t>
+MediaMetricsService::getSanitizedPackageNameAndVersionCode(uid_t uid) {
+ // Meyer's singleton, initialized on first access.
+ // mUidInfo is locked internally.
+ static mediautils::UidInfo uidInfo;
+
+ // get info.
+ mediautils::UidInfo::Info info = uidInfo.getInfo(uid);
+ if (useUidForPackage(info.package, info.installer)) {
+ return { std::to_string(uid), /* versionCode */ 0 };
+ } else {
+ return { info.package, info.versionCode };
+ }
+}
+
+/* static */
+std::string MediaMetricsService::tokenizer(std::string::const_iterator& it,
+ const std::string::const_iterator& end, const char *reserved) {
+ // consume leading white space
+ for (; it != end && std::isspace(*it); ++it);
+ if (it == end) return {};
+
+ auto start = it;
+ // parse until we hit a reserved keyword or space
+ if (strchr(reserved, *it)) return {start, ++it};
+ for (;;) {
+ ++it;
+ if (it == end || std::isspace(*it) || strchr(reserved, *it)) return {start, it};
+ }
+}
+
+/* static */
+std::vector<std::pair<std::string, std::string>>
+MediaMetricsService::getDeviceAddressPairs(const std::string& devices) {
+ std::vector<std::pair<std::string, std::string>> result;
+
+ // Currently, the device format is EXACTLY
+ // (device1, addr1)|(device2, addr2)|...
+
+ static constexpr char delim[] = "()|,";
+ for (auto it = devices.begin(); ; ) {
+ auto token = tokenizer(it, devices.end(), delim);
+ if (token != "(") return result;
+
+ auto device = tokenizer(it, devices.end(), delim);
+ if (device.empty() || !std::isalnum(device[0])) return result;
+
+ token = tokenizer(it, devices.end(), delim);
+ if (token != ",") return result;
+
+ // special handling here for empty addresses
+ auto address = tokenizer(it, devices.end(), delim);
+ if (address.empty() || !std::isalnum(device[0])) return result;
+ if (address == ")") { // no address, just the ")"
+ address.clear();
+ } else {
+ token = tokenizer(it, devices.end(), delim);
+ if (token != ")") return result;
+ }
+
+ result.emplace_back(std::move(device), std::move(address));
+
+ token = tokenizer(it, devices.end(), delim);
+ if (token != "|") return result; // this includes end of string detection
+ }
+}
+
MediaMetricsService::MediaMetricsService()
: mMaxRecords(kMaxRecords),
mMaxRecordAgeNs(kMaxRecordAgeNs),
@@ -136,16 +204,10 @@
// Overwrite package name and version if the caller was untrusted or empty
if (!isTrusted || item->getPkgName().empty()) {
const uid_t uid = item->getUid();
- mediautils::UidInfo::Info info = mUidInfo.getInfo(uid);
- if (useUidForPackage(info.package, info.installer)) {
- // remove uid information of unknown installed packages.
- // TODO: perhaps this can be done just before uploading to Westworld.
- item->setPkgName(std::to_string(uid));
- item->setPkgVersionCode(0);
- } else {
- item->setPkgName(info.package);
- item->setPkgVersionCode(info.versionCode);
- }
+ const auto [ pkgName, version ] =
+ MediaMetricsService::getSanitizedPackageNameAndVersionCode(uid);
+ item->setPkgName(pkgName);
+ item->setPkgVersionCode(version);
}
ALOGV("%s: isTrusted:%d given uid %d; sanitized uid: %d sanitized pkg: %s "
diff --git a/services/mediametrics/MediaMetricsService.h b/services/mediametrics/MediaMetricsService.h
index faba197..b8eb267 100644
--- a/services/mediametrics/MediaMetricsService.h
+++ b/services/mediametrics/MediaMetricsService.h
@@ -69,6 +69,28 @@
*/
static bool useUidForPackage(const std::string& package, const std::string& installer);
+ /**
+ * Returns a std::pair of packageName and versionCode for a given uid.
+ *
+ * The value is sanitized - i.e. if the result is not approved to send,
+ * we use the uid as a string and a version code of 0.
+ */
+ static std::pair<std::string, int64_t> getSanitizedPackageNameAndVersionCode(uid_t uid);
+
+ /**
+ * Return string tokens from iterator, separated by spaces and reserved chars.
+ */
+ static std::string tokenizer(std::string::const_iterator& it,
+ const std::string::const_iterator& end, const char *reserved);
+
+ /**
+ * Parse the devices string and return a vector of device address pairs.
+ *
+ * A failure to parse returns early with the contents that were able to be parsed.
+ */
+ static std::vector<std::pair<std::string, std::string>>
+ getDeviceAddressPairs(const std::string &devices);
+
protected:
// Internal call where release is true if ownership of item is transferred
@@ -100,8 +122,6 @@
std::atomic<int64_t> mItemsSubmitted{}; // accessed outside of lock.
- mediautils::UidInfo mUidInfo; // mUidInfo can be accessed without lock (locked internally)
-
mediametrics::AudioAnalytics mAudioAnalytics; // mAudioAnalytics is locked internally.
std::mutex mLock;
diff --git a/services/mediametrics/TimedAction.h b/services/mediametrics/TimedAction.h
new file mode 100644
index 0000000..c7ef585
--- /dev/null
+++ b/services/mediametrics/TimedAction.h
@@ -0,0 +1,103 @@
+/*
+ * 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 <android-base/thread_annotations.h>
+#include <chrono>
+#include <map>
+#include <mutex>
+#include <thread>
+
+namespace android::mediametrics {
+
+class TimedAction {
+public:
+ TimedAction() : mThread{[this](){threadLoop();}} {}
+
+ ~TimedAction() {
+ quit();
+ }
+
+ // TODO: return a handle for cancelling the action?
+ template <typename T> // T is in units of std::chrono::duration.
+ void postIn(const T& time, std::function<void()> f) {
+ postAt(std::chrono::steady_clock::now() + time, f);
+ }
+
+ template <typename T> // T is in units of std::chrono::time_point
+ void postAt(const T& targetTime, std::function<void()> f) {
+ std::lock_guard l(mLock);
+ if (mQuit) return;
+ if (mMap.empty() || targetTime < mMap.begin()->first) {
+ mMap.emplace_hint(mMap.begin(), targetTime, std::move(f));
+ mCondition.notify_one();
+ } else {
+ mMap.emplace(targetTime, std::move(f));
+ }
+ }
+
+ void clear() {
+ std::lock_guard l(mLock);
+ mMap.clear();
+ }
+
+ void quit() {
+ {
+ std::lock_guard l(mLock);
+ if (mQuit) return;
+ mQuit = true;
+ mMap.clear();
+ mCondition.notify_all();
+ }
+ mThread.join();
+ }
+
+ size_t size() const {
+ std::lock_guard l(mLock);
+ return mMap.size();
+ }
+
+private:
+ void threadLoop() NO_THREAD_SAFETY_ANALYSIS { // thread safety doesn't cover unique_lock
+ std::unique_lock l(mLock);
+ while (!mQuit) {
+ auto sleepUntilTime = std::chrono::time_point<std::chrono::steady_clock>::max();
+ if (!mMap.empty()) {
+ sleepUntilTime = mMap.begin()->first;
+ if (sleepUntilTime <= std::chrono::steady_clock::now()) {
+ auto node = mMap.extract(mMap.begin()); // removes from mMap.
+ l.unlock();
+ node.mapped()();
+ l.lock();
+ continue;
+ }
+ }
+ mCondition.wait_until(l, sleepUntilTime);
+ }
+ }
+
+ mutable std::mutex mLock;
+ std::condition_variable mCondition GUARDED_BY(mLock);
+ bool mQuit GUARDED_BY(mLock) = false;
+ std::multimap<std::chrono::time_point<std::chrono::steady_clock>, std::function<void()>>
+ mMap GUARDED_BY(mLock); // multiple functions could execute at the same time.
+
+ // needs to be initialized after the variables above, done in constructor initializer list.
+ std::thread mThread;
+};
+
+} // namespace android::mediametrics
diff --git a/services/mediametrics/tests/mediametrics_tests.cpp b/services/mediametrics/tests/mediametrics_tests.cpp
index cf0dceb..b465ecd 100644
--- a/services/mediametrics/tests/mediametrics_tests.cpp
+++ b/services/mediametrics/tests/mediametrics_tests.cpp
@@ -883,6 +883,48 @@
}
}
+TEST(mediametrics_tests, device_parsing) {
+ auto devaddr = android::MediaMetricsService::getDeviceAddressPairs("(DEVICE, )");
+ ASSERT_EQ((size_t)1, devaddr.size());
+ ASSERT_EQ("DEVICE", devaddr[0].first);
+ ASSERT_EQ("", devaddr[0].second);
+
+ devaddr = android::MediaMetricsService::getDeviceAddressPairs(
+ "(DEVICE1, A)|(D, ADDRB)");
+ ASSERT_EQ((size_t)2, devaddr.size());
+ ASSERT_EQ("DEVICE1", devaddr[0].first);
+ ASSERT_EQ("A", devaddr[0].second);
+ ASSERT_EQ("D", devaddr[1].first);
+ ASSERT_EQ("ADDRB", devaddr[1].second);
+
+ devaddr = android::MediaMetricsService::getDeviceAddressPairs(
+ "(A,B)|(C,D)");
+ ASSERT_EQ((size_t)2, devaddr.size());
+ ASSERT_EQ("A", devaddr[0].first);
+ ASSERT_EQ("B", devaddr[0].second);
+ ASSERT_EQ("C", devaddr[1].first);
+ ASSERT_EQ("D", devaddr[1].second);
+
+ devaddr = android::MediaMetricsService::getDeviceAddressPairs(
+ " ( A1 , B ) | ( C , D2 ) ");
+ ASSERT_EQ((size_t)2, devaddr.size());
+ ASSERT_EQ("A1", devaddr[0].first);
+ ASSERT_EQ("B", devaddr[0].second);
+ ASSERT_EQ("C", devaddr[1].first);
+ ASSERT_EQ("D2", devaddr[1].second);
+}
+
+TEST(mediametrics_tests, timed_action) {
+ android::mediametrics::TimedAction timedAction;
+ std::atomic_int value1 = 0;
+
+ timedAction.postIn(std::chrono::seconds(0), [&value1] { ++value1; });
+ timedAction.postIn(std::chrono::seconds(1000), [&value1] { ++value1; });
+ usleep(100000);
+ ASSERT_EQ(1, value1);
+ ASSERT_EQ((size_t)1, timedAction.size());
+}
+
#if 0
// Stress test code for garbage collection, you need to enable AID_SHELL as trusted to run
// in MediaMetricsService.cpp.