Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c3e89810 authored by Andy Hung's avatar Andy Hung Committed by Automerger Merge Worker
Browse files

Merge "MediaMetrics: Add device-based actions for Bluetooth" into rvc-dev am: b3cedfdf

Change-Id: I322d05af73b99f4f949ef3be7b47a0ba62361b30
parents c6979dd5 b3cedfdf
Loading
Loading
Loading
Loading
+9 −28
Original line number Diff line number Diff line
@@ -78,8 +78,8 @@ public:
    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 @@ public:
        std::vector<Action> actions;
        std::lock_guard l(mLock);

        // Essentially the code looks like this:
        /*
        for (auto &[trigger, action] : mFilters) {
            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: there should only be one match for a non-wildcard url.
        auto it = mFilters.upper_bound( {item->getKey(), std::monostate{} });
        if (it != mFilters.end()) {
            const auto &[trigger, action] = *it;
            if (isMatch(trigger, item)) {
                actions.push_back(action);
            }
        }
        // TODO: Optimize for prefix search and wildcarding.

        // Optimization: for wildcard URLs we go backwards until there is no
        // match with the prefix before the wildcard.
        while (it != mFilters.begin()) {  // this walks backwards, cannot start at begin.
            const auto &[trigger, action] = *--it;  // look backwards
            int ret = isWildcardMatch(trigger, item);
            if (ret == mediametrics::Item::RECURSIVE_WILDCARD_CHECK_MATCH_FOUND) {
                actions.push_back(action);    // match found.
            } else if (ret == mediametrics::Item::RECURSIVE_WILDCARD_CHECK_NO_MATCH_NO_WILDCARD) {
                break;                        // no match before wildcard.
            }
            // a wildcard was encountered when matching prefix, so we should check again.
        }
        return actions;
    }

@@ -145,7 +124,9 @@ private:
    }

    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
+1 −0
Original line number Diff line number Diff line
@@ -76,5 +76,6 @@ cc_library_shared {
        "-Wall",
        "-Werror",
        "-Wextra",
        "-Wthread-safety",
    ],
}
+263 −1
Original line number Diff line number Diff line
@@ -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

// Enable for testing of delivery to statsd
// #define STATSD

namespace android::mediametrics {

@@ -87,11 +91,53 @@ AudioAnalytics::AudioAnalytics()
                    // 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 @@ std::string AudioAnalytics::getThreadFromTrack(const std::string& track) const
    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
+52 −0
Original line number Diff line number Diff line
@@ -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 @@ private:
     */
    std::string getThreadFromTrack(const std::string& track) const;

    const bool mDeliverStatistics __unused = true;

    // Actions is individually locked
    AnalyticsActions mActions;

@@ -97,6 +101,54 @@ private:

    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
+72 −10
Original line number Diff line number Diff line
@@ -78,6 +78,74 @@ bool MediaMetricsService::useUidForPackage(
    }
}

/* 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 @@ status_t MediaMetricsService::submitInternal(mediametrics::Item *item, bool rele
    // 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 "
Loading