MediaMetrics: Update and clean proto formatting

Make code compatible with both string
and integer enumeration in the protos.

Test: adb shell dumpsys media.metrics
Test: atest mediametrics_tests
Bug: 149850236
Change-Id: I0a1c21be711f82915f3bb403860b40c695a0587d
diff --git a/services/mediametrics/Android.bp b/services/mediametrics/Android.bp
index 645d151..f819f1b 100644
--- a/services/mediametrics/Android.bp
+++ b/services/mediametrics/Android.bp
@@ -119,6 +119,7 @@
     srcs: [
         "AudioAnalytics.cpp",
         "AudioPowerUsage.cpp",
+        "AudioTypes.cpp",
         "iface_statsd.cpp",
         "MediaMetricsService.cpp",
         "statsd_audiopolicy.cpp",
@@ -130,6 +131,7 @@
         "statsd_extractor.cpp",
         "statsd_nuplayer.cpp",
         "statsd_recorder.cpp",
+        "StringUtils.cpp"
     ],
 
     proto: {
@@ -137,9 +139,11 @@
     },
 
     shared_libs: [
+        "libbase", // android logging
         "libbinder",
         "libcutils",
         "liblog",
+        "libmedia_helper",
         "libmediametrics",
         "libmediautils",
         "libmemunreachable",
diff --git a/services/mediametrics/AudioAnalytics.cpp b/services/mediametrics/AudioAnalytics.cpp
index 31ad234..800f099 100644
--- a/services/mediametrics/AudioAnalytics.cpp
+++ b/services/mediametrics/AudioAnalytics.cpp
@@ -16,20 +16,45 @@
 
 //#define LOG_NDEBUG 0
 #define LOG_TAG "AudioAnalytics"
+#include <android-base/logging.h>
 #include <utils/Log.h>
 
 #include "AudioAnalytics.h"
-#include "MediaMetricsService.h"  // package info
+
 #include <audio_utils/clock.h>    // clock conversions
+#include <cutils/properties.h>
 #include <statslog.h>             // statsd
 
+#include "AudioTypes.h"           // string to int conversions
+#include "MediaMetricsService.h"  // package info
+#include "StringUtils.h"
+
+#define PROP_AUDIO_ANALYTICS_CLOUD_ENABLED "persist.audio.analytics.cloud.enabled"
+
 // Enable for testing of delivery to statsd
-// #define STATSD
+//#define STATSD
+
+// Transmit to statsd in integer or strings
+//#define USE_INT
+
+#ifdef USE_INT
+using short_enum_type_t = int32_t;
+using long_enum_type_t = int64_t;
+#define ENUM_EXTRACT(x) (x)
+#else
+using short_enum_type_t = std::string;
+using long_enum_type_t = std::string;
+#define ENUM_EXTRACT(x) (x).c_str()
+#endif
+
+using android::base::DEBUG;
 
 namespace android::mediametrics {
 
 AudioAnalytics::AudioAnalytics()
+    : mDeliverStatistics(property_get_bool(PROP_AUDIO_ANALYTICS_CLOUD_ENABLED, true))
 {
+    SetMinimumLogSeverity(DEBUG); // for LOG().
     ALOGD("%s", __func__);
 
     // Add action to save AnalyticsState if audioserver is restarted.
@@ -243,33 +268,47 @@
     int32_t frameCount = 0;
     mAudioAnalytics.mAnalyticsState->timeMachine().get(
             key, AMEDIAMETRICS_PROP_FRAMECOUNT, &frameCount);
-    std::string inputDevices;
+    std::string inputDevicePairs;
     mAudioAnalytics.mAnalyticsState->timeMachine().get(
-            key, AMEDIAMETRICS_PROP_INPUTDEVICES, &inputDevices);
+            key, AMEDIAMETRICS_PROP_INPUTDEVICES, &inputDevicePairs);
     int32_t intervalCount = 0;
     mAudioAnalytics.mAnalyticsState->timeMachine().get(
             key, AMEDIAMETRICS_PROP_INTERVALCOUNT, &intervalCount);
-    std::string outputDevices;
+    std::string outputDevicePairs;
     mAudioAnalytics.mAnalyticsState->timeMachine().get(
-            key, AMEDIAMETRICS_PROP_OUTPUTDEVICES, &outputDevices);
+            key, AMEDIAMETRICS_PROP_OUTPUTDEVICES, &outputDevicePairs);
     int32_t sampleRate = 0;
     mAudioAnalytics.mAnalyticsState->timeMachine().get(
             key, AMEDIAMETRICS_PROP_SAMPLERATE, &sampleRate);
     std::string flags;
     mAudioAnalytics.mAnalyticsState->timeMachine().get(
             key, AMEDIAMETRICS_PROP_FLAGS, &flags);
+
     // We may have several devices.
-    // Strings allow us to mix input and output devices together.
-    // TODO: review if we want to separate them.
-    std::stringstream ss;
-    for (const auto& devicePairs : { outputDevices, inputDevices }) {
-        const auto devaddrvec = MediaMetricsService::getDeviceAddressPairs(devicePairs);
+    // Accumulate the bit flags for input and output devices.
+    std::stringstream oss;
+    long_enum_type_t outputDeviceBits{};
+    {   // compute outputDevices
+        const auto devaddrvec = stringutils::getDeviceAddressPairs(outputDevicePairs);
         for (const auto& [device, addr] : devaddrvec) {
-            if (ss.tellp() > 0) ss << "|";  // delimit devices with '|'.
-            ss << device;
+            if (oss.tellp() > 0) oss << "|";  // delimit devices with '|'.
+            oss << device;
+            outputDeviceBits += types::lookup<types::OUTPUT_DEVICE, long_enum_type_t>(device);
         }
     }
-    std::string devices = ss.str();
+    const std::string outputDevices = oss.str();
+
+    std::stringstream iss;
+    long_enum_type_t inputDeviceBits{};
+    {   // compute inputDevices
+        const auto devaddrvec = stringutils::getDeviceAddressPairs(inputDevicePairs);
+        for (const auto& [device, addr] : devaddrvec) {
+            if (iss.tellp() > 0) iss << "|";  // delimit devices with '|'.
+            iss << device;
+            inputDeviceBits += types::lookup<types::INPUT_DEVICE, long_enum_type_t>(device);
+        }
+    }
+    const std::string inputDevices = iss.str();
 
     // Get connected device name if from bluetooth.
     bool isBluetooth = false;
@@ -278,8 +317,8 @@
         isBluetooth = true;
         mAudioAnalytics.mAnalyticsState->timeMachine().get(
             "audio.device.bt_a2dp", AMEDIAMETRICS_PROP_NAME, &deviceNames);
-        // We don't check if deviceName is sanitized.
-        // TODO: remove reserved chars such as '|' and replace with a char like '_'.
+        // Remove | if present
+        stringutils::replace(deviceNames, "|", '?');
     }
 
     switch (itemType) {
@@ -305,37 +344,43 @@
         mAudioAnalytics.mAnalyticsState->timeMachine().get(
                 key, AMEDIAMETRICS_PROP_SOURCE, &source);
 
-        ALOGD("(key=%s) id:%s endAudioIntervalGroup devices:%s deviceNames:%s "
-                 "deviceTimeNs:%lld encoding:%s frameCount:%d intervalCount:%d "
-                 "sampleRate:%d "
-                 "packageName:%s "
-                 "selectedDeviceId:%d "
-                 "callerName:%s source:%s",
-                key.c_str(), id.c_str(), devices.c_str(), deviceNames.c_str(),
-                (long long)deviceTimeNs, encoding.c_str(), frameCount, intervalCount,
-                sampleRate,
-                packageName.c_str(), selectedDeviceId,
-                callerName.c_str(), source.c_str());
+        const auto callerNameForStats =
+                types::lookup<types::CALLER_NAME, short_enum_type_t>(callerName);
+        const auto encodingForStats = types::lookup<types::ENCODING, short_enum_type_t>(encoding);
+        const auto flagsForStats = types::lookup<types::INPUT_FLAG, short_enum_type_t>(flags);
+        const auto sourceForStats = types::lookup<types::SOURCE_TYPE, short_enum_type_t>(source);
 
+        LOG(DEBUG) << "key:" << key
+              << " id:" << id
+              << " inputDevices:" << inputDevices << "(" << inputDeviceBits
+              << ") deviceNames:" << deviceNames
+              << " deviceTimeNs:" << deviceTimeNs
+              << " encoding:" << encoding << "(" << encodingForStats
+              << ") frameCount:" << frameCount
+              << " intervalCount:" << intervalCount
+              << " sampleRate:" << sampleRate
+              << " flags:" << flags << "(" << flagsForStats
+              << ") packageName:" << packageName
+              << " selectedDeviceId:" << selectedDeviceId
+              << " callerName:" << callerName << "(" << callerNameForStats
+              << ") source:" << source << "(" << sourceForStats << ")";
 #ifdef STATSD
         if (mAudioAnalytics.mDeliverStatistics) {
             (void)android::util::stats_write(
                     android::util::MEDIAMETRICS_AUDIORECORDDEVICEUSAGE_REPORTED
-                    /* timestamp, */
-                    /* mediaApexVersion, */
-                    , devices.c_str()
+                    , ENUM_EXTRACT(inputDeviceBits)
                     , deviceNames.c_str()
                     , deviceTimeNs
-                    , encoding.c_str()
+                    , ENUM_EXTRACT(encodingForStats)
                     , frameCount
                     , intervalCount
                     , sampleRate
-                    , flags.c_str()
+                    , ENUM_EXTRACT(flagsForStats)
 
                     , packageName.c_str()
                     , selectedDeviceId
-                    , callerName.c_str()
-                    , source.c_str()
+                    , ENUM_EXTRACT(callerNameForStats)
+                    , ENUM_EXTRACT(sourceForStats)
                     );
         }
 #endif
@@ -347,31 +392,43 @@
         int32_t underrun = 0; // zero for record types
         mAudioAnalytics.mAnalyticsState->timeMachine().get(
                 key, AMEDIAMETRICS_PROP_UNDERRUN, &underrun);
-        ALOGD("(key=%s) id:%s endAudioIntervalGroup devices:%s deviceNames:%s "
-                 "deviceTimeNs:%lld encoding:%s frameCount:%d intervalCount:%d "
-                 "sampleRate:%d underrun:%d "
-                 "flags:%s type:%s",
-                key.c_str(), id.c_str(), devices.c_str(), deviceNames.c_str(),
-                (long long)deviceTimeNs, encoding.c_str(), frameCount, intervalCount,
-                sampleRate, underrun,
-                flags.c_str(), type.c_str());
+
+        const bool isInput = types::isInputThreadType(type);
+        const auto encodingForStats = types::lookup<types::ENCODING, short_enum_type_t>(encoding);
+        const auto flagsForStats =
+                (isInput ? types::lookup<types::INPUT_FLAG, short_enum_type_t>(flags)
+                        : types::lookup<types::OUTPUT_FLAG, short_enum_type_t>(flags));
+        const auto typeForStats = types::lookup<types::THREAD_TYPE, short_enum_type_t>(type);
+
+        LOG(DEBUG) << "key:" << key
+              << " id:" << id
+              << " inputDevices:" << inputDevices << "(" << inputDeviceBits
+              << ") outputDevices:" << outputDevices << "(" << outputDeviceBits
+              << ") deviceNames:" << deviceNames
+              << " deviceTimeNs:" << deviceTimeNs
+              << " encoding:" << encoding << "(" << encodingForStats
+              << ") frameCount:" << frameCount
+              << " intervalCount:" << intervalCount
+              << " sampleRate:" << sampleRate
+              << " underrun:" << underrun
+              << " flags:" << flags << "(" << flagsForStats
+              << ") type:" << type << "(" << typeForStats
+              << ")";
 #ifdef STATSD
         if (mAudioAnalytics.mDeliverStatistics) {
             (void)android::util::stats_write(
                 android::util::MEDIAMETRICS_AUDIOTHREADDEVICEUSAGE_REPORTED
-                /* timestamp, */
-                /* mediaApexVersion, */
-                , devices.c_str()
+                , ENUM_EXTRACT(inputDeviceBits)
+                , ENUM_EXTRACT(outputDeviceBits)
                 , deviceNames.c_str()
                 , deviceTimeNs
-                , encoding.c_str()
+                , ENUM_EXTRACT(encodingForStats)
                 , frameCount
                 , intervalCount
                 , sampleRate
-                , flags.c_str()
-
+                , ENUM_EXTRACT(flagsForStats)
                 , underrun
-                , type.c_str()
+                , ENUM_EXTRACT(typeForStats)
             );
         }
 #endif
@@ -420,34 +477,51 @@
         mAudioAnalytics.mAnalyticsState->timeMachine().get(
                 key, AMEDIAMETRICS_PROP_USAGE, &usage);
 
-        ALOGD("(key=%s) id:%s endAudioIntervalGroup devices:%s deviceNames:%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 "
-                 "selectedDeviceId:%d streamType:%s usage:%s",
-                key.c_str(), id.c_str(), devices.c_str(), deviceNames.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, streamType.c_str(), usage.c_str());
+        const auto callerNameForStats =
+                types::lookup<types::CALLER_NAME, short_enum_type_t>(callerName);
+        const auto contentTypeForStats =
+                types::lookup<types::CONTENT_TYPE, short_enum_type_t>(contentType);
+        const auto encodingForStats = types::lookup<types::ENCODING, short_enum_type_t>(encoding);
+        const auto flagsForStats = types::lookup<types::OUTPUT_FLAG, short_enum_type_t>(flags);
+        const auto streamTypeForStats =
+                types::lookup<types::STREAM_TYPE, short_enum_type_t>(streamType);
+        const auto usageForStats = types::lookup<types::USAGE, short_enum_type_t>(usage);
+
+        LOG(DEBUG) << "key:" << key
+              << " id:" << id
+              << " outputDevices:" << outputDevices << "(" << outputDeviceBits
+              << ") deviceNames:" << deviceNames
+              << " deviceTimeNs:" << deviceTimeNs
+              << " encoding:" << encoding << "(" << encodingForStats
+              << ") frameCount:" << frameCount
+              << " intervalCount:" << intervalCount
+              << " sampleRate:" << sampleRate
+              << " underrun:" << underrun
+              << " flags:" << flags << "(" << flagsForStats
+              << ") callerName:" << callerName << "(" << callerNameForStats
+              << ") contentType:" << contentType << "(" << contentTypeForStats
+              << ") deviceLatencyMs:" << deviceLatencyMs
+              << " deviceStartupMs:" << deviceStartupMs
+              << " deviceVolume:" << deviceVolume
+              << " packageName:" << packageName
+              << " playbackPitch:" << playbackPitch
+              << " playbackSpeed:" << playbackSpeed
+              << " selectedDeviceId:" << selectedDeviceId
+              << " streamType:" << streamType << "(" << streamTypeForStats
+              << ") usage:" << usage << "(" << usageForStats
+              << ")";
 #ifdef STATSD
         if (mAudioAnalytics.mDeliverStatistics) {
             (void)android::util::stats_write(
                     android::util::MEDIAMETRICS_AUDIOTRACKDEVICEUSAGE_REPORTED
-                    /* timestamp, */
-                    /* mediaApexVersion, */
-                    , devices.c_str()
+                    , ENUM_EXTRACT(outputDeviceBits)
                     , deviceNames.c_str()
                     , deviceTimeNs
-                    , encoding.c_str()
+                    , ENUM_EXTRACT(encodingForStats)
                     , frameCount
                     , intervalCount
                     , sampleRate
-                    , flags.c_str()
+                    , ENUM_EXTRACT(flagsForStats)
                     , underrun
 
                     , packageName.c_str()
@@ -455,10 +529,10 @@
                     , (float)deviceStartupMs
                     , (float)deviceVolume
                     , selectedDeviceId
-                    , streamType.c_str()
-                    , usage.c_str()
-                    , contentType.c_str()
-                    , callerName.c_str()
+                    , ENUM_EXTRACT(streamTypeForStats)
+                    , ENUM_EXTRACT(usageForStats)
+                    , ENUM_EXTRACT(contentTypeForStats)
+                    , ENUM_EXTRACT(callerNameForStats)
                     );
         }
 #endif
@@ -490,7 +564,6 @@
     item->get(AMEDIAMETRICS_PROP_NAME, &name);
     ALOGD("(key=%s) a2dp connected device:%s atNs:%lld",
             key.c_str(), name.c_str(), (long long)atNs);
-
 }
 
 void AudioAnalytics::DeviceConnection::createPatch(
@@ -502,27 +575,36 @@
     item->get(AMEDIAMETRICS_PROP_OUTPUTDEVICES, &outputDevices);
     if (outputDevices.find("AUDIO_DEVICE_OUT_BLUETOOTH_A2DP") != std::string::npos) {
         // TODO compare address
-        int64_t timeDiff = item->getTimestamp();
+        int64_t timeDiffNs = item->getTimestamp();
         if (mA2dpConnectionRequestNs == 0) {
             ALOGD("%s: A2DP create patch didn't see a connection request", __func__);
-            timeDiff -= mA2dpConnectionServiceNs;
+            timeDiffNs -= mA2dpConnectionServiceNs;
         } else {
-            timeDiff -= mA2dpConnectionRequestNs;
+            timeDiffNs -= mA2dpConnectionRequestNs;
         }
-        ALOGD("(key=%s) A2DP device connection time: %lld", key.c_str(), (long long)timeDiff);
+
         mA2dpConnectionRequestNs = 0;
         mA2dpConnectionServiceNs = 0;
         ++mA2dpConnectionSuccesses;
 
+        const auto connectionTimeMs = float(timeDiffNs * 1e-6);
+
+        const auto outputDeviceBits = types::lookup<types::OUTPUT_DEVICE, long_enum_type_t>(
+                "AUDIO_DEVICE_OUT_BLUETOOTH_A2DP");
+
+        LOG(DEBUG) << "key:" << key
+                << " A2DP SUCCESS"
+                << " outputDevices:" << outputDeviceBits
+                << " connectionTimeMs:" <<  connectionTimeMs;
 #ifdef STATSD
         if (mAudioAnalytics.mDeliverStatistics) {
+            const long_enum_type_t inputDeviceBits{};
             (void)android::util::stats_write(
                     android::util::MEDIAMETRICS_AUDIODEVICECONNECTION_REPORTED
-                    /* timestamp, */
-                    /* mediaApexVersion, */
-                    , "AUDIO_DEVICE_OUT_BLUETOOTH_A2DP"
-                    , android::util::MEDIAMETRICS_AUDIO_DEVICE_CONNECTION_REPORTED__RESULT__SUCCESS
-                    , /* connection_time_ms */ timeDiff * 1e-6 /* NS to MS */
+                    , ENUM_EXTRACT(inputDeviceBits)
+                    , ENUM_EXTRACT(outputDeviceBits)
+                    , types::DEVICE_CONNECTION_RESULT_SUCCESS
+                    , connectionTimeMs
                     , /* connection_count */ 1
                     );
         }
@@ -552,18 +634,25 @@
 void AudioAnalytics::DeviceConnection::expire() {
     std::lock_guard l(mLock);
     if (mA2dpConnectionRequestNs == 0) return; // ignore (this was an internal connection).
+
+#ifdef STATSD
+    const long_enum_type_t inputDeviceBits{};
+#endif
+    const auto outputDeviceBits = types::lookup<types::OUTPUT_DEVICE, long_enum_type_t>(
+            "AUDIO_DEVICE_OUT_BLUETOOTH_A2DP");
+
     if (mA2dpConnectionServiceNs == 0) {
-        ALOGD("A2DP device connection service cancels");
         ++mA2dpConnectionJavaServiceCancels;  // service did not connect to A2DP
 
+        LOG(DEBUG) << "A2DP CANCEL"
+                << " outputDevices:" << outputDeviceBits;
 #ifdef STATSD
         if (mAudioAnalytics.mDeliverStatistics) {
             (void)android::util::stats_write(
                     android::util::MEDIAMETRICS_AUDIODEVICECONNECTION_REPORTED
-                    /* timestamp, */
-                    /* mediaApexVersion, */
-                    , "AUDIO_DEVICE_OUT_BLUETOOTH_A2DP"
-                    , android::util::MEDIAMETRICS_AUDIO_DEVICE_CONNECTION_REPORTED__RESULT__JAVA_SERVICE_CANCEL
+                    , ENUM_EXTRACT(inputDeviceBits)
+                    , ENUM_EXTRACT(outputDeviceBits)
+                    , types::DEVICE_CONNECTION_RESULT_JAVA_SERVICE_CANCEL
                     , /* connection_time_ms */ 0.f
                     , /* connection_count */ 1
                     );
@@ -575,18 +664,19 @@
     // AudioFlinger didn't play - an expiration may occur because there is no audio playing.
     // Should we check elsewhere?
     // TODO: disambiguate this case.
-    ALOGD("A2DP device connection expired, state unknown");
     mA2dpConnectionRequestNs = 0;
     mA2dpConnectionServiceNs = 0;
     ++mA2dpConnectionUnknowns;  // connection result unknown
+
+    LOG(DEBUG) << "A2DP UNKNOWN"
+            << " outputDevices:" << outputDeviceBits;
 #ifdef STATSD
     if (mAudioAnalytics.mDeliverStatistics) {
         (void)android::util::stats_write(
                 android::util::MEDIAMETRICS_AUDIODEVICECONNECTION_REPORTED
-                /* timestamp, */
-                /* mediaApexVersion, */
-                , "AUDIO_DEVICE_OUT_BLUETOOTH_A2DP"
-                , android::util::MEDIAMETRICS_AUDIO_DEVICE_CONNECTION_REPORTED__RESULT__UNKNOWN
+                , ENUM_EXTRACT(inputDeviceBits)
+                , ENUM_EXTRACT(outputDeviceBits)
+                , types::DEVICE_CONNECTION_RESULT_UNKNOWN
                 , /* connection_time_ms */ 0.f
                 , /* connection_count */ 1
                 );
diff --git a/services/mediametrics/AudioAnalytics.h b/services/mediametrics/AudioAnalytics.h
index 9089d6f..138ddcc 100644
--- a/services/mediametrics/AudioAnalytics.h
+++ b/services/mediametrics/AudioAnalytics.h
@@ -109,7 +109,7 @@
      */
     std::string getThreadFromTrack(const std::string& track) const;
 
-    const bool mDeliverStatistics __unused = true;
+    const bool mDeliverStatistics;
 
     // Actions is individually locked
     AnalyticsActions mActions;
diff --git a/services/mediametrics/AudioPowerUsage.cpp b/services/mediametrics/AudioPowerUsage.cpp
index b1648d9..c441110 100644
--- a/services/mediametrics/AudioPowerUsage.cpp
+++ b/services/mediametrics/AudioPowerUsage.cpp
@@ -20,6 +20,7 @@
 
 #include "AudioAnalytics.h"
 #include "MediaMetricsService.h"
+#include "StringUtils.h"
 #include <map>
 #include <sstream>
 #include <string>
@@ -117,7 +118,7 @@
 
 int32_t AudioPowerUsage::deviceFromStringPairs(const std::string& device_strings) {
     int32_t deviceMask = 0;
-    const auto devaddrvec = MediaMetricsService::getDeviceAddressPairs(device_strings);
+    const auto devaddrvec = stringutils::getDeviceAddressPairs(device_strings);
     for (const auto &[device, addr] : devaddrvec) {
         int32_t combo_device = 0;
         deviceFromString(device, combo_device);
diff --git a/services/mediametrics/AudioTypes.cpp b/services/mediametrics/AudioTypes.cpp
new file mode 100644
index 0000000..2a2dbaf
--- /dev/null
+++ b/services/mediametrics/AudioTypes.cpp
@@ -0,0 +1,383 @@
+/*
+ * 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.
+ */
+
+#include "AudioTypes.h"
+#include "StringUtils.h"
+#include <media/TypeConverter.h> // requires libmedia_helper to get the Audio code.
+
+namespace android::mediametrics::types {
+
+std::unordered_map<std::string, int64_t>& getAudioDeviceInMap() {
+    // DO NOT MODIFY VALUES (OK to add new ones).  This does NOT match audio_device_t.
+    static std::unordered_map<std::string, int64_t> map{
+        {"AUDIO_DEVICE_IN_COMMUNICATION",          1LL << 0},
+        {"AUDIO_DEVICE_IN_AMBIENT",                1LL << 1},
+        {"AUDIO_DEVICE_IN_BUILTIN_MIC",            1LL << 2},
+        {"AUDIO_DEVICE_IN_BLUETOOTH_SCO_HEADSET",  1LL << 3},
+        {"AUDIO_DEVICE_IN_WIRED_HEADSET",          1LL << 4},
+        {"AUDIO_DEVICE_IN_AUX_DIGITAL",            1LL << 5},
+        {"AUDIO_DEVICE_IN_HDMI",                   1LL << 5}, // HDMI == AUX_DIGITAL (6 reserved)
+        {"AUDIO_DEVICE_IN_VOICE_CALL",             1LL << 7},
+        {"AUDIO_DEVICE_IN_TELEPHONY_RX",           1LL << 7}, // TELEPHONY_RX == VOICE_CALL (8 reserved)
+        {"AUDIO_DEVICE_IN_BACK_MIC",               1LL << 9},
+        {"AUDIO_DEVICE_IN_REMOTE_SUBMIX",          1LL << 10},
+        {"AUDIO_DEVICE_IN_ANLG_DOCK_HEADSET",      1LL << 11},
+        {"AUDIO_DEVICE_IN_DGTL_DOCK_HEADSET",      1LL << 12},
+        {"AUDIO_DEVICE_IN_USB_ACCESSORY",          1LL << 13},
+        {"AUDIO_DEVICE_IN_USB_DEVICE",             1LL << 14},
+        {"AUDIO_DEVICE_IN_FM_TUNER",               1LL << 15},
+        {"AUDIO_DEVICE_IN_TV_TUNER",               1LL << 16},
+        {"AUDIO_DEVICE_IN_LINE",                   1LL << 17},
+        {"AUDIO_DEVICE_IN_SPDIF",                  1LL << 18},
+        {"AUDIO_DEVICE_IN_BLUETOOTH_A2DP",         1LL << 19},
+        {"AUDIO_DEVICE_IN_LOOPBACK",               1LL << 20},
+        {"AUDIO_DEVICE_IN_IP",                     1LL << 21},
+        {"AUDIO_DEVICE_IN_BUS",                    1LL << 22},
+        {"AUDIO_DEVICE_IN_PROXY",                  1LL << 23},
+        {"AUDIO_DEVICE_IN_USB_HEADSET",            1LL << 24},
+        {"AUDIO_DEVICE_IN_BLUETOOTH_BLE",          1LL << 25},
+        {"AUDIO_DEVICE_IN_HDMI_ARC",               1LL << 26},
+        {"AUDIO_DEVICE_IN_ECHO_REFERENCE",         1LL << 27},
+        {"AUDIO_DEVICE_IN_DEFAULT",                1LL << 28},
+        // R values above.
+    };
+    return map;
+}
+
+std::unordered_map<std::string, int64_t>& getAudioDeviceOutMap() {
+    // DO NOT MODIFY VALUES (OK to add new ones).  This does NOT match audio_device_t.
+    static std::unordered_map<std::string, int64_t> map{
+        {"AUDIO_DEVICE_OUT_EARPIECE",                  1LL << 0},
+        {"AUDIO_DEVICE_OUT_SPEAKER",                   1LL << 1},
+        {"AUDIO_DEVICE_OUT_WIRED_HEADSET",             1LL << 2},
+        {"AUDIO_DEVICE_OUT_WIRED_HEADPHONE",           1LL << 3},
+        {"AUDIO_DEVICE_OUT_BLUETOOTH_SCO",             1LL << 4},
+        {"AUDIO_DEVICE_OUT_BLUETOOTH_SCO_HEADSET",     1LL << 5},
+        {"AUDIO_DEVICE_OUT_BLUETOOTH_SCO_CARKIT",      1LL << 6},
+        {"AUDIO_DEVICE_OUT_BLUETOOTH_A2DP",            1LL << 7},
+        {"AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES", 1LL << 8},
+        {"AUDIO_DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER",    1LL << 9},
+        {"AUDIO_DEVICE_OUT_AUX_DIGITAL",               1LL << 10},
+        {"AUDIO_DEVICE_OUT_HDMI",                      1LL << 10}, // HDMI == AUX_DIGITAL (11 reserved)
+        {"AUDIO_DEVICE_OUT_ANLG_DOCK_HEADSET",         1LL << 12},
+        {"AUDIO_DEVICE_OUT_DGTL_DOCK_HEADSET",         1LL << 13},
+        {"AUDIO_DEVICE_OUT_USB_ACCESSORY",             1LL << 14},
+        {"AUDIO_DEVICE_OUT_USB_DEVICE",                1LL << 15},
+        {"AUDIO_DEVICE_OUT_REMOTE_SUBMIX",             1LL << 16},
+        {"AUDIO_DEVICE_OUT_TELEPHONY_TX",              1LL << 17},
+        {"AUDIO_DEVICE_OUT_LINE",                      1LL << 18},
+        {"AUDIO_DEVICE_OUT_HDMI_ARC",                  1LL << 19},
+        {"AUDIO_DEVICE_OUT_SPDIF",                     1LL << 20},
+        {"AUDIO_DEVICE_OUT_FM",                        1LL << 21},
+        {"AUDIO_DEVICE_OUT_AUX_LINE",                  1LL << 22},
+        {"AUDIO_DEVICE_OUT_SPEAKER_SAFE",              1LL << 23},
+        {"AUDIO_DEVICE_OUT_IP",                        1LL << 24},
+        {"AUDIO_DEVICE_OUT_BUS",                       1LL << 25},
+        {"AUDIO_DEVICE_OUT_PROXY",                     1LL << 26},
+        {"AUDIO_DEVICE_OUT_USB_HEADSET",               1LL << 27},
+        {"AUDIO_DEVICE_OUT_HEARING_AID",               1LL << 28},
+        {"AUDIO_DEVICE_OUT_ECHO_CANCELLER",            1LL << 29},
+        {"AUDIO_DEVICE_OUT_DEFAULT",                   1LL << 30},
+        // R values above.
+    };
+    return map;
+}
+
+std::unordered_map<std::string, int32_t>& getCallerNameMap() {
+    // DO NOT MODIFY VALUES (OK to add new ones).
+    // This may be found in frameworks/av/media/libmediametrics/include/MediaMetricsConstants.h
+    static std::unordered_map<std::string, int32_t> map{
+        {"aaudio",        0},           // Native AAudio
+        {"java",          1},           // Java API layer
+        {"media",         2},           // libmedia (mediaplayer)
+        {"opensles",      3},           // Open SLES
+        {"rtp",           4},           // RTP communication
+        {"soundpool",     5},           // SoundPool
+        {"tonegenerator", 6},           // dial tones
+        {"unknown",       7},           // callerName not set
+        // R values above.
+    };
+    return map;
+}
+
+std::unordered_map<std::string, int32_t>& getThreadTypeMap() {
+    // DO NOT MODIFY VALUES (OK to add new ones).
+    // This may be found in frameworks/av/services/audioflinger/Threads.h
+    static std::unordered_map<std::string, int32_t> map{
+        // UNKNOWN is -1
+        {"MIXER",         0},          // Thread class is MixerThread
+        {"DIRECT",        1},          // Thread class is DirectOutputThread
+        {"DUPLICATING",   2},          // Thread class is DuplicatingThread
+        {"RECORD",        3},          // Thread class is RecordThread
+        {"OFFLOAD",       4},          // Thread class is OffloadThread
+        {"MMAP_PLAYBACK", 5},          // Thread class for MMAP playback stream
+        {"MMAP_CAPTURE",  6},          // Thread class for MMAP capture stream
+        // R values above.
+    };
+    return map;
+}
+
+// Helper: Create the corresponding int32 from string flags split with '|'.
+template <typename Traits>
+int32_t int32FromFlags(const std::string &flags)
+{
+    const auto result = stringutils::split(flags, "|");
+    int32_t intFlags = 0;
+    for (const auto& flag : result) {
+        typename Traits::Type value;
+        if (!TypeConverter<Traits>::fromString(flag, value)) {
+            break;
+        }
+        intFlags |= value;
+    }
+    return intFlags;
+}
+
+template <typename Traits>
+std::string stringFromFlags(const std::string &flags, size_t len)
+{
+    const auto result = stringutils::split(flags, "|");
+    std::string sFlags;
+    for (const auto& flag : result) {
+        typename Traits::Type value;
+        if (!TypeConverter<Traits>::fromString(flag, value)) {
+            break;
+        }
+        if (len >= flag.size()) continue;
+        if (!sFlags.empty()) sFlags += "|";
+        sFlags += flag.c_str() + len;
+    }
+    return sFlags;
+}
+
+template <>
+int32_t lookup<CONTENT_TYPE>(const std::string &contentType)
+{
+    AudioContentTraits::Type value;
+    if (!TypeConverter<AudioContentTraits>::fromString(contentType, value)) {
+        value = AUDIO_CONTENT_TYPE_UNKNOWN;
+    }
+    return (int32_t)value;
+}
+
+template <>
+std::string lookup<CONTENT_TYPE>(const std::string &contentType)
+{
+    AudioContentTraits::Type value;
+    if (!TypeConverter<AudioContentTraits>::fromString(contentType, value)) {
+        return "UNKNOWN";
+    }
+    return contentType.c_str() + sizeof("AUDIO_CONTENT_TYPE");
+}
+
+template <>
+int32_t lookup<ENCODING>(const std::string &encoding)
+{
+    FormatTraits::Type value;
+    if (!TypeConverter<FormatTraits>::fromString(encoding, value)) {
+        value = AUDIO_FORMAT_INVALID;
+    }
+    return (int32_t)value;
+}
+
+template <>
+std::string lookup<ENCODING>(const std::string &encoding)
+{
+    FormatTraits::Type value;
+    if (!TypeConverter<FormatTraits>::fromString(encoding, value)) {
+        return "INVALID";
+    }
+    return encoding.c_str() + sizeof("AUDIO_FORMAT");
+}
+
+template <>
+int32_t lookup<INPUT_FLAG>(const std::string &inputFlag)
+{
+    return int32FromFlags<InputFlagTraits>(inputFlag);
+}
+
+template <>
+std::string lookup<INPUT_FLAG>(const std::string &inputFlag)
+{
+    return stringFromFlags<InputFlagTraits>(inputFlag, sizeof("AUDIO_INPUT_FLAG"));
+}
+
+template <>
+int32_t lookup<OUTPUT_FLAG>(const std::string &outputFlag)
+{
+    return int32FromFlags<OutputFlagTraits>(outputFlag);
+}
+
+template <>
+std::string lookup<OUTPUT_FLAG>(const std::string &outputFlag)
+{
+    return stringFromFlags<OutputFlagTraits>(outputFlag, sizeof("AUDIO_OUTPUT_FLAG"));
+}
+
+template <>
+int32_t lookup<SOURCE_TYPE>(const std::string &sourceType)
+{
+    SourceTraits::Type value;
+    if (!TypeConverter<SourceTraits>::fromString(sourceType, value)) {
+        value = AUDIO_SOURCE_DEFAULT;
+    }
+    return (int32_t)value;
+}
+
+template <>
+std::string lookup<SOURCE_TYPE>(const std::string &sourceType)
+{
+    SourceTraits::Type value;
+    if (!TypeConverter<SourceTraits>::fromString(sourceType, value)) {
+        return "DEFAULT";
+    }
+    return sourceType.c_str() + sizeof("AUDIO_SOURCE");
+}
+
+template <>
+int32_t lookup<STREAM_TYPE>(const std::string &streamType)
+{
+    StreamTraits::Type value;
+    if (!TypeConverter<StreamTraits>::fromString(streamType, value)) {
+        value = AUDIO_STREAM_DEFAULT;
+    }
+    return (int32_t)value;
+}
+
+template <>
+std::string lookup<STREAM_TYPE>(const std::string &streamType)
+{
+    StreamTraits::Type value;
+    if (!TypeConverter<StreamTraits>::fromString(streamType, value)) {
+        return "DEFAULT";
+    }
+    return streamType.c_str() + sizeof("AUDIO_STREAM");
+}
+
+template <>
+int32_t lookup<USAGE>(const std::string &usage)
+{
+    UsageTraits::Type value;
+    if (!TypeConverter<UsageTraits>::fromString(usage, value)) {
+        value = AUDIO_USAGE_UNKNOWN;
+    }
+    return (int32_t)value;
+}
+
+template <>
+std::string lookup<USAGE>(const std::string &usage)
+{
+    UsageTraits::Type value;
+    if (!TypeConverter<UsageTraits>::fromString(usage, value)) {
+        return "UNKNOWN";
+    }
+    return usage.c_str() + sizeof("AUDIO_USAGE");
+}
+
+template <>
+int64_t lookup<INPUT_DEVICE>(const std::string &inputDevice)
+{
+    auto& map = getAudioDeviceInMap();
+    auto it = map.find(inputDevice);
+    if (it == map.end()) {
+        return 0;
+    }
+    return it->second;
+}
+
+template <>
+std::string lookup<INPUT_DEVICE>(const std::string &inputDevice)
+{
+    auto& map = getAudioDeviceInMap();
+    auto it = map.find(inputDevice);
+    if (it == map.end()) {
+        return "NONE";
+    }
+    return inputDevice.c_str() + sizeof("AUDIO_DEVICE_IN");
+}
+
+template <>
+int64_t lookup<OUTPUT_DEVICE>(const std::string &outputDevice)
+{
+    auto& map = getAudioDeviceOutMap();
+    auto it = map.find(outputDevice);
+    if (it == map.end()) {
+        return 0; // nothing
+    }
+    return it->second;
+}
+
+template <>
+std::string lookup<OUTPUT_DEVICE>(const std::string &outputDevice)
+{
+    auto& map = getAudioDeviceOutMap();
+    auto it = map.find(outputDevice);
+    if (it == map.end()) {
+        return "NONE";
+    }
+    return outputDevice.c_str() + sizeof("AUDIO_DEVICE_OUT");
+}
+
+template <>
+int32_t lookup<CALLER_NAME>(const std::string &callerName)
+{
+    auto& map = getCallerNameMap();
+    auto it = map.find(callerName);
+    if (it == map.end()) {
+        return 7;      // return unknown
+    }
+    return it->second;
+}
+
+template <>
+std::string lookup<CALLER_NAME>(const std::string &callerName)
+{
+    auto& map = getCallerNameMap();
+    auto it = map.find(callerName);
+    if (it == map.end()) {
+        return "unknown";
+    }
+    return callerName;
+}
+
+template <>
+int32_t lookup<THREAD_TYPE>(const std::string &threadType)
+{
+    auto& map = getThreadTypeMap();
+    auto it = map.find(threadType);
+    if (it == map.end()) {
+        return -1; // note this as an illegal thread value as we don't have unknown here.
+    }
+    return it->second;
+}
+
+template <>
+std::string lookup<THREAD_TYPE>(const std::string &threadType)
+{
+    auto& map = getThreadTypeMap();
+    auto it = map.find(threadType);
+    if (it == map.end()) {
+        return "UNKNOWN";
+    }
+    return threadType;
+}
+
+bool isInputThreadType(const std::string &threadType)
+{
+    return threadType == "RECORD" || threadType == "MMAP_CAPTURE";
+}
+
+} // namespace android::mediametrics::types
diff --git a/services/mediametrics/AudioTypes.h b/services/mediametrics/AudioTypes.h
new file mode 100644
index 0000000..a094e6e
--- /dev/null
+++ b/services/mediametrics/AudioTypes.h
@@ -0,0 +1,74 @@
+/*
+ * 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 <string>
+#include <unordered_map>
+
+namespace android::mediametrics::types {
+
+// Helper methods that map mediametrics logged strings to
+// integer codes.
+std::unordered_map<std::string, int64_t>& getAudioDeviceInMap();
+std::unordered_map<std::string, int64_t>& getAudioDeviceOutMap();
+std::unordered_map<std::string, int32_t>& getCallerNameMap();
+std::unordered_map<std::string, int32_t>& getThreadTypeMap();
+
+// Enumeration for the device connection results.
+enum DeviceConnectionResult : int32_t {
+    DEVICE_CONNECTION_RESULT_UNKNOWN = 0,              // Success is unknown.
+    DEVICE_CONNECTION_RESULT_SUCCESS = 1,              // Audio delivered
+    DEVICE_CONNECTION_RESULT_JAVA_SERVICE_CANCEL = 2,  // Canceled in Java service
+    // Do not modify the constants above after R.  Adding new constants is fine.
+};
+
+// Enumeration for all the string translations to integers (generally int32_t) unless noted.
+enum AudioEnumCategory {
+    CALLER_NAME,
+    CONTENT_TYPE,
+    ENCODING,
+    INPUT_DEVICE,  // int64_t
+    INPUT_FLAG,
+    OUTPUT_DEVICE, // int64_t
+    OUTPUT_FLAG,
+    SOURCE_TYPE,
+    STREAM_TYPE,
+    THREAD_TYPE,
+    USAGE,
+};
+
+// Convert a string (or arbitrary S) from an AudioEnumCategory to a particular type.
+// This is used to convert log std::strings back to the original type (int32_t or int64_t).
+//
+// For a string, generally there is a prefix "AUDIO_INPUT_FLAG" or some such that could
+// actually indicate the category so the AudioEnumCategory could be superfluous, but
+// we use it to find the proper default value in case of an unknown string.
+//
+// lookup<ENCODING, int32_t>("AUDIO_FORMAT_PCM_16_BIT") -> 1
+//
+template <AudioEnumCategory C, typename T, typename S>
+T lookup(const S &str);
+
+// Helper: Allow using a const char * in lieu of std::string.
+template <AudioEnumCategory C, typename T>
+T lookup(const char *str) {
+    return lookup<C, T, std::string>(str);
+}
+
+bool isInputThreadType(const std::string &threadType);
+
+} // namespace android::mediametrics::types
diff --git a/services/mediametrics/MediaMetricsService.cpp b/services/mediametrics/MediaMetricsService.cpp
index 584bd13..d682fed 100644
--- a/services/mediametrics/MediaMetricsService.cpp
+++ b/services/mediametrics/MediaMetricsService.cpp
@@ -95,58 +95,6 @@
     }
 }
 
-/* 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),
diff --git a/services/mediametrics/MediaMetricsService.h b/services/mediametrics/MediaMetricsService.h
index b8eb267..d152264 100644
--- a/services/mediametrics/MediaMetricsService.h
+++ b/services/mediametrics/MediaMetricsService.h
@@ -77,20 +77,6 @@
      */
     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
diff --git a/services/mediametrics/StringUtils.cpp b/services/mediametrics/StringUtils.cpp
new file mode 100644
index 0000000..50525bc
--- /dev/null
+++ b/services/mediametrics/StringUtils.cpp
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+//#define LOG_NDEBUG 0
+#define LOG_TAG "MediaMetricsService::stringutils"
+#include <utils/Log.h>
+
+#include "StringUtils.h"
+
+namespace android::mediametrics::stringutils {
+
+std::string 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};
+    }
+}
+
+std::vector<std::string> split(const std::string& flags, const char *delim)
+{
+    std::vector<std::string> result;
+    for (auto it = flags.begin(); ; ) {
+        auto flag = tokenizer(it, flags.end(), delim);
+        if (flag.empty() || !std::isalnum(flag[0])) return result;
+        result.emplace_back(std::move(flag));
+
+        // look for the delimeter and discard
+        auto token = tokenizer(it, flags.end(), delim);
+        if (token.size() != 1 || strchr(delim, token[0]) == nullptr) return result;
+    }
+}
+
+std::vector<std::pair<std::string, std::string>> 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
+    }
+}
+
+size_t replace(std::string &str, const char *targetChars, const char replaceChar)
+{
+    size_t replaced = 0;
+    for (char &c : str) {
+        if (strchr(targetChars, c) != nullptr) {
+            c = replaceChar;
+            ++replaced;
+        }
+    }
+    return replaced;
+}
+
+} // namespace android::mediametrics::stringutils
diff --git a/services/mediametrics/StringUtils.h b/services/mediametrics/StringUtils.h
new file mode 100644
index 0000000..d878720
--- /dev/null
+++ b/services/mediametrics/StringUtils.h
@@ -0,0 +1,47 @@
+/*
+ * 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 <string>
+#include <vector>
+
+namespace android::mediametrics::stringutils {
+
+/**
+ * Return string tokens from iterator, separated by spaces and reserved chars.
+ */
+std::string tokenizer(std::string::const_iterator& it,
+        const std::string::const_iterator& end, const char *reserved);
+
+/**
+ * Splits flags string based on delimeters (or, whitespace which is removed).
+ */
+std::vector<std::string> split(const std::string& flags, const char *delim);
+
+/**
+ * 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.
+ */
+std::vector<std::pair<std::string, std::string>> getDeviceAddressPairs(const std::string &devices);
+
+/**
+ * Replaces targetChars with replaceChar in string, returns number of chars replaced.
+ */
+size_t replace(std::string &str, const char *targetChars, const char replaceChar);
+
+} // namespace android::mediametrics::stringutils
diff --git a/services/mediametrics/tests/mediametrics_tests.cpp b/services/mediametrics/tests/mediametrics_tests.cpp
index f7988f1..7da6306 100644
--- a/services/mediametrics/tests/mediametrics_tests.cpp
+++ b/services/mediametrics/tests/mediametrics_tests.cpp
@@ -18,6 +18,7 @@
 #include <utils/Log.h>
 
 #include "MediaMetricsService.h"
+#include "StringUtils.h"
 
 #include <stdio.h>
 
@@ -884,12 +885,12 @@
 }
 
 TEST(mediametrics_tests, device_parsing) {
-    auto devaddr = android::MediaMetricsService::getDeviceAddressPairs("(DEVICE, )");
+    auto devaddr = android::mediametrics::stringutils::getDeviceAddressPairs("(DEVICE, )");
     ASSERT_EQ((size_t)1, devaddr.size());
     ASSERT_EQ("DEVICE", devaddr[0].first);
     ASSERT_EQ("", devaddr[0].second);
 
-    devaddr = android::MediaMetricsService::getDeviceAddressPairs(
+    devaddr = android::mediametrics::stringutils::getDeviceAddressPairs(
             "(DEVICE1, A)|(D, ADDRB)");
     ASSERT_EQ((size_t)2, devaddr.size());
     ASSERT_EQ("DEVICE1", devaddr[0].first);
@@ -897,7 +898,7 @@
     ASSERT_EQ("D", devaddr[1].first);
     ASSERT_EQ("ADDRB", devaddr[1].second);
 
-    devaddr = android::MediaMetricsService::getDeviceAddressPairs(
+    devaddr = android::mediametrics::stringutils::getDeviceAddressPairs(
             "(A,B)|(C,D)");
     ASSERT_EQ((size_t)2, devaddr.size());
     ASSERT_EQ("A", devaddr[0].first);
@@ -905,7 +906,7 @@
     ASSERT_EQ("C", devaddr[1].first);
     ASSERT_EQ("D", devaddr[1].second);
 
-    devaddr = android::MediaMetricsService::getDeviceAddressPairs(
+    devaddr = android::mediametrics::stringutils::getDeviceAddressPairs(
             "  ( A1 , B )  | ( C , D2 )  ");
     ASSERT_EQ((size_t)2, devaddr.size());
     ASSERT_EQ("A1", devaddr[0].first);