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.