MediaMetrics: Add logSessionId handling

Test: adb shell dumpsys media.metrics
Test: atest mediametrics_tests
Bug: 181032765
Change-Id: I030555abeaf2f286b61a85032bc767581c2e605e
diff --git a/services/mediametrics/AudioAnalytics.cpp b/services/mediametrics/AudioAnalytics.cpp
index 3b2de76..9b7da0f 100644
--- a/services/mediametrics/AudioAnalytics.cpp
+++ b/services/mediametrics/AudioAnalytics.cpp
@@ -87,6 +87,7 @@
     "selected_device_id",
     "caller",
     "source",
+    "log_session_id",
 };
 
 static constexpr const char * const AudioThreadDeviceUsageFields[] = {
@@ -124,6 +125,7 @@
     "content_type",
     "caller",
     "traits",
+    "log_session_id",
 };
 
 static constexpr const char * const AudioDeviceConnectionFields[] = {
@@ -521,12 +523,18 @@
         std::string source;
         mAudioAnalytics.mAnalyticsState->timeMachine().get(
                 key, AMEDIAMETRICS_PROP_SOURCE, &source);
+        // Android S
+        std::string logSessionId;
+        mAudioAnalytics.mAnalyticsState->timeMachine().get(
+                key, AMEDIAMETRICS_PROP_LOGSESSIONID, &logSessionId);
 
         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);
+        // Android S
+        const auto logSessionIdForStats = stringutils::sanitizeLogSessionId(logSessionId);
 
         LOG(LOG_LEVEL) << "key:" << key
               << " id:" << id
@@ -541,7 +549,9 @@
               << ") packageName:" << packageName
               << " selectedDeviceId:" << selectedDeviceId
               << " callerName:" << callerName << "(" << callerNameForStats
-              << ") source:" << source << "(" << sourceForStats << ")";
+              << ") source:" << source << "(" << sourceForStats
+              << ") logSessionId:" << logSessionId << "(" << logSessionIdForStats
+              << ")";
         if (clientCalled  // only log if client app called AudioRecord.
                 && mAudioAnalytics.mDeliverStatistics) {
             const auto [ result, str ] = sendToStatsd(AudioRecordDeviceUsageFields,
@@ -559,6 +569,7 @@
                     , selectedDeviceId
                     , ENUM_EXTRACT(callerNameForStats)
                     , ENUM_EXTRACT(sourceForStats)
+                    , logSessionIdForStats.c_str()
                     );
             ALOGV("%s: statsd %s", __func__, str.c_str());
             mAudioAnalytics.mStatsdLog.log("%s", str.c_str());
@@ -659,6 +670,10 @@
         std::string usage;
         mAudioAnalytics.mAnalyticsState->timeMachine().get(
                 key, AMEDIAMETRICS_PROP_USAGE, &usage);
+        // Android S
+        std::string logSessionId;
+        mAudioAnalytics.mAnalyticsState->timeMachine().get(
+                key, AMEDIAMETRICS_PROP_LOGSESSIONID, &logSessionId);
 
         const auto callerNameForStats =
                 types::lookup<types::CALLER_NAME, short_enum_type_t>(callerName);
@@ -671,6 +686,8 @@
         const auto traitsForStats =
                  types::lookup<types::TRACK_TRAITS, short_enum_type_t>(traits);
         const auto usageForStats = types::lookup<types::USAGE, short_enum_type_t>(usage);
+        // Android S
+        const auto logSessionIdForStats = stringutils::sanitizeLogSessionId(logSessionId);
 
         LOG(LOG_LEVEL) << "key:" << key
               << " id:" << id
@@ -695,6 +712,7 @@
               << " streamType:" << streamType << "(" << streamTypeForStats
               << ") traits:" << traits << "(" << traitsForStats
               << ") usage:" << usage << "(" << usageForStats
+              << ") logSessionId:" << logSessionId << "(" << logSessionIdForStats
               << ")";
         if (clientCalled // only log if client app called AudioTracks
                 && mAudioAnalytics.mDeliverStatistics) {
@@ -719,6 +737,7 @@
                     , ENUM_EXTRACT(contentTypeForStats)
                     , ENUM_EXTRACT(callerNameForStats)
                     , ENUM_EXTRACT(traitsForStats)
+                    , logSessionIdForStats.c_str()
                     );
             ALOGV("%s: statsd %s", __func__, str.c_str());
             mAudioAnalytics.mStatsdLog.log("%s", str.c_str());
diff --git a/services/mediametrics/StringUtils.h b/services/mediametrics/StringUtils.h
index 7a8bbee..37ed173 100644
--- a/services/mediametrics/StringUtils.h
+++ b/services/mediametrics/StringUtils.h
@@ -68,4 +68,82 @@
  */
 size_t replace(std::string &str, const char *targetChars, const char replaceChar);
 
+// RFC 1421, 2045, 2152, 4648(4), 4880
+inline constexpr char Base64Table[] =
+    // 0000000000111111111122222222223333333333444444444455555555556666
+    // 0123456789012345678901234567890123456789012345678901234567890123
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+// RFC 4648(5) URL-safe Base64 encoding
+inline constexpr char Base64UrlTable[] =
+    // 0000000000111111111122222222223333333333444444444455555555556666
+    // 0123456789012345678901234567890123456789012345678901234567890123
+    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+
+// An constexpr struct that transposes/inverts a string conversion table.
+struct Transpose {
+    // constexpr bug, returning char still means -1 == 0xff, so we use unsigned char.
+    using base_char_t = unsigned char;
+    static inline constexpr base_char_t INVALID_CHAR = 0xff;
+
+    template <size_t N>
+    explicit constexpr Transpose(const char(&string)[N]) {
+        for (auto& e : mMap) {
+            e = INVALID_CHAR;
+        }
+        for (size_t i = 0; string[i] != 0; ++i) {
+            mMap[static_cast<size_t>(string[i]) & 0xff] = i;
+        }
+    }
+
+    constexpr base_char_t operator[] (size_t n) const {
+        return n < sizeof(mMap) ? mMap[n] : INVALID_CHAR;
+    }
+
+    constexpr const auto& get() const {
+        return mMap;
+    }
+
+private:
+    base_char_t mMap[256];  // construct an inverse character mapping.
+};
+
+// This table is used to convert an input char to a 6 bit (0 - 63) value.
+// If the input char is not in the Base64Url charset, Transpose::INVALID_CHAR is returned.
+inline constexpr Transpose InverseBase64UrlTable(Base64UrlTable);
+
+// Returns true if s consists of only valid Base64Url characters (no padding chars allowed).
+inline constexpr bool isBase64Url(const char *s) {
+    for (; *s != 0; ++s) {
+        if (InverseBase64UrlTable[(unsigned char)*s] == Transpose::INVALID_CHAR) return false;
+    }
+    return true;
+}
+
+// Returns true if s is a valid log session id: exactly 16 Base64Url characters.
+//
+// logSessionIds are a web-safe Base64Url RFC 4648(5) encoded string of 16 characters
+// (representing 96 unique bits 16 * 6).
+//
+// The string version is considered the reference representation.  However, for ease of
+// manipulation and comparison, it may be converted to an int128.
+//
+// For int128 conversion, some common interpretations exist - for example
+// (1) the 16 Base64 chars can be converted 6 bits per char to a 96 bit value
+// (with the most significant 32 bits as zero) as there are only 12 unique bytes worth of data
+// or (2) the 16 Base64 chars can be used to directly fill the 128 bits of int128 assuming
+// the 16 chars are 16 bytes, filling the layout of the int128 variable.
+// Endianness of the data may follow whatever is convenient in the interpretation as long
+// as it is applied to each such conversion of string to int128 identically.
+//
+inline constexpr bool isLogSessionId(const char *s) {
+    return std::char_traits<std::decay_t<decltype(*s)>>::length(s) == 16 && isBase64Url(s);
+}
+
+// Returns either the original string or an empty string if isLogSessionId check fails.
+inline std::string sanitizeLogSessionId(const std::string& string) {
+    if (isLogSessionId(string.c_str())) return string;
+    return {}; // if not a logSessionId, return an empty string.
+}
+
 } // namespace android::mediametrics::stringutils
diff --git a/services/mediametrics/statsd_audiorecord.cpp b/services/mediametrics/statsd_audiorecord.cpp
index 76f4b59..db809dc 100644
--- a/services/mediametrics/statsd_audiorecord.cpp
+++ b/services/mediametrics/statsd_audiorecord.cpp
@@ -32,6 +32,7 @@
 #include <statslog.h>
 
 #include "MediaMetricsService.h"
+#include "StringUtils.h"
 #include "frameworks/proto_logging/stats/enums/stats/mediametrics/mediametrics.pb.h"
 #include "iface_statsd.h"
 
@@ -134,19 +135,26 @@
         metrics_proto.set_start_count(startcount);
     }
 
-
     std::string serialized;
     if (!metrics_proto.SerializeToString(&serialized)) {
         ALOGE("Failed to serialize audiorecord metrics");
         return false;
     }
 
+    // Android S
+    // log_session_id (string)
+    std::string logSessionId;
+    (void)item->getString("android.media.audiorecord.logSessionId", &logSessionId);
+    const auto logSessionIdForStats =
+            mediametrics::stringutils::sanitizeLogSessionId(logSessionId);
+
     if (enabled_statsd) {
         android::util::BytesField bf_serialized( serialized.c_str(), serialized.size());
         (void)android::util::stats_write(android::util::MEDIAMETRICS_AUDIORECORD_REPORTED,
                                    timestamp, pkgName.c_str(), pkgVersionCode,
                                    mediaApexVersion,
-                                   bf_serialized);
+                                   bf_serialized,
+                                   logSessionIdForStats.c_str());
 
     } else {
         ALOGV("NOT sending: private data (len=%zu)", strlen(serialized.c_str()));
diff --git a/services/mediametrics/statsd_audiotrack.cpp b/services/mediametrics/statsd_audiotrack.cpp
index 6b08a78..fd809c8 100644
--- a/services/mediametrics/statsd_audiotrack.cpp
+++ b/services/mediametrics/statsd_audiotrack.cpp
@@ -32,6 +32,7 @@
 #include <statslog.h>
 
 #include "MediaMetricsService.h"
+#include "StringUtils.h"
 #include "frameworks/proto_logging/stats/enums/stats/mediametrics/mediametrics.pb.h"
 #include "iface_statsd.h"
 
@@ -132,12 +133,20 @@
         return false;
     }
 
+    // Android S
+    // log_session_id (string)
+    std::string logSessionId;
+    (void)item->getString("android.media.audiotrack.logSessionId", &logSessionId);
+    const auto logSessionIdForStats =
+            mediametrics::stringutils::sanitizeLogSessionId(logSessionId);
+
     if (enabled_statsd) {
         android::util::BytesField bf_serialized( serialized.c_str(), serialized.size());
         (void)android::util::stats_write(android::util::MEDIAMETRICS_AUDIOTRACK_REPORTED,
                                    timestamp, pkgName.c_str(), pkgVersionCode,
                                    mediaApexVersion,
-                                   bf_serialized);
+                                   bf_serialized,
+                                   logSessionIdForStats.c_str());
 
     } else {
         ALOGV("NOT sending: private data (len=%zu)", strlen(serialized.c_str()));
diff --git a/services/mediametrics/tests/mediametrics_tests.cpp b/services/mediametrics/tests/mediametrics_tests.cpp
index 478355b..ac9c7fa 100644
--- a/services/mediametrics/tests/mediametrics_tests.cpp
+++ b/services/mediametrics/tests/mediametrics_tests.cpp
@@ -1082,3 +1082,42 @@
   //mediaMetrics->dump(fileno(stdout), {} /* args */);
 }
 #endif
+
+// Base64Url and isLogSessionId string utilities can be tested by static asserts.
+static_assert(mediametrics::stringutils::isBase64Url("abc"));
+static_assert(mediametrics::stringutils::InverseBase64UrlTable['A'] == 0);
+static_assert(mediametrics::stringutils::InverseBase64UrlTable['a'] == 26);
+static_assert(mediametrics::stringutils::InverseBase64UrlTable['!'] ==
+        mediametrics::stringutils::Transpose::INVALID_CHAR);
+static_assert(mediametrics::stringutils::InverseBase64UrlTable['@'] ==
+        mediametrics::stringutils::Transpose::INVALID_CHAR);
+static_assert(mediametrics::stringutils::InverseBase64UrlTable['#'] ==
+        mediametrics::stringutils::Transpose::INVALID_CHAR);
+static_assert(!mediametrics::stringutils::isBase64Url("!@#"));
+
+static_assert(mediametrics::stringutils::isLogSessionId("0123456789abcdef"));
+static_assert(!mediametrics::stringutils::isLogSessionId("abc"));
+static_assert(!mediametrics::stringutils::isLogSessionId("!@#"));
+static_assert(!mediametrics::stringutils::isLogSessionId("0123456789abcde!"));
+
+TEST(mediametrics_tests, sanitizeLogSessionId) {
+   // invalid id returns empty string.
+   ASSERT_EQ("", mediametrics::stringutils::sanitizeLogSessionId("abc"));
+
+   // valid id passes through.
+   std::string validId = "fedcba9876543210";
+   ASSERT_EQ(validId, mediametrics::stringutils::sanitizeLogSessionId(validId));
+
+   // one more char makes the id invalid
+   ASSERT_EQ("", mediametrics::stringutils::sanitizeLogSessionId(validId + "A"));
+
+   std::string validId2 = "ZYXWVUT123456789";
+   ASSERT_EQ(validId2, mediametrics::stringutils::sanitizeLogSessionId(validId2));
+
+   // one fewer char makes the id invalid
+   ASSERT_EQ("", mediametrics::stringutils::sanitizeLogSessionId(validId.c_str() + 1));
+
+   // replacing one character with an invalid character makes an invalid id.
+   validId2[3] = '!';
+   ASSERT_EQ("", mediametrics::stringutils::sanitizeLogSessionId(validId2));
+}